var debug = false; //document.location.href.match('api');

var images = {
	plus: chrome.extension.getURL('images/plus.png'),
	minus: chrome.extension.getURL('images/minus.png'),
	preferences: chrome.extension.getURL('images/icon16.png'),
	reload: chrome.extension.getURL('images/reload.png'),
	convert: chrome.extension.getURL('images/reload.png'),
	mailicon: chrome.extension.getURL('images/mailicon.png'),
	color: chrome.extension.getURL('images/color.png'),
	remove: chrome.extension.getURL('images/remove.png'),
	lock: chrome.extension.getURL('images/lock.png'),
	down: chrome.extension.getURL('images/down.png'),
	up: chrome.extension.getURL('images/up.png'),
	error: chrome.extension.getURL('images/error.png'),
	ok: chrome.extension.getURL('images/ok.png'),
	updating: chrome.extension.getURL('images/pulser2.gif'),
	spaceout: chrome.extension.getURL('images/spaceout.gif'),
	paypal: chrome.extension.getURL('images/paypal.png'),
        amazon: chrome.extension.getURL('images/amazon.png'),
        bitcoin: chrome.extension.getURL('images/bitcoin.png')
};

var installPage = 'https://chrome.google.com/webstore/detail/edbpbkkonoaiiflfhbmklndghkddlnfo';

// Greased MooTools
// included via manifest
// end Greased MooTools

var preferences = undefined;

// hack to circumvent 'bug' when overriding toString (and others):
// https://mootools.lighthouseapp.com/projects/2706/tickets/651-classtostring-broken-on-122-big-regression
['toString', 'toLocaleString', 'valueOf', 'toSource', 'watch', 'unwatch', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable'].each(function(method) {
	Class.Mutators[method] = $arguments(0);
});

var x11Colors = [ // http://www.w3.org/TR/2002/WD-css3-color-20020219/#x11-color
'AliceBlue', 'AntiqueWhite', 'Aqua', 'Aquamarine', 'Azure', 'Beige', 'Bisque', 'Black', 'BlanchedAlmond', 'Blue', 'BlueViolet', 'Brown', 'BurlyWood', 'CadetBlue', 'Chartreuse', 'Chocolate', 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', 'Cyan', 'DarkBlue', 'DarkCyan', 'DarkGoldenrod', 'DarkGray', 'DarkGreen', 'DarkKhaki', 'DarkMagenta', 'DarkOliveGreen', 'DarkOrange', 'DarkOrchid', 'DarkRed', 'DarkSalmon', 'DarkSeaGreen', 'DarkSlateBlue', 'DarkSlateGray', 'DarkTurquoise', 'DarkViolet', 'DeepPink', 'DeepSkyBlue', 'DimGray', 'DodgerBlue', 'FireBrick', 'FloralWhite', 'ForestGreen', 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', 'Goldenrod', 'Gray', 'Green', 'GreenYellow', 'Honeydew', 'HotPink', 'IndianRed', 'Indigo', 'Ivory', 'Khaki', 'Lavender', 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', 'LightCoral', 'LightCyan', 'LightGoldenrodYellow', 'LightGreen', 'LightGrey', 'LightPink', 'LightSalmon', 'LightSeaGreen', 'LightSkyBlue', 'LightSlateGray', 'LightSteelBlue', 'LightYellow', 'Lime', 'LimeGreen', 'Linen', 'Magenta', 'Maroon', 'MediumAquamarine', 'MediumBlue', 'MediumOrchid', 'MediumPurple', 'MediumSeaGreen', 'MediumSlateBlue', 'MediumSpringGreen', 'MediumTurquoise', 'MediumVioletRed', 'MidnightBlue', 'MintCream', 'MistyRose', 'Moccasin', 'NavajoWhite', 'Navy', 'OldLace', 'Olive', 'OliveDrab', 'Orange', 'OrangeRed', 'Orchid', 'PaleGoldenrod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', 'Plum', 'PowderBlue', 'Purple', 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', 'Salmon', 'SandyBrown', 'SeaGreen', 'Seashell', 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', 'SlateGray', 'Snow', 'SpringGreen', 'SteelBlue', 'Tan', 'Teal', 'Thistle', 'Tomato', 'Turquoise', 'Violet', 'Wheat', 'White', 'WhiteSmoke', 'Yellow', 'YellowGreen'];

// Greasemonkey helpers and wrappers
// included via manifest
// end Greasemonkey helpers and wrappers

// only run when the user is logged in
var loggedIn = $chk(GM_getUserNsid());
if (!loggedIn) {
	throw new Error("not logged in; aborting");
}

// the following api_key is reserved for this application
// if you need an api_key for your own application, please request one at 
// http://www.flickr.com/services/apps/create/apply/
// if you request a Non-Commercial key, you'll get it instantly
var api_key = 'f34fa6453715520a1a280fa8cee46ec4'; // the app's own key
if (debug) GM_log("DEBUGGING");

// version check code
	function showUpdateNotification(data) {
		var onlineVersion = data.onlineVersion;
		var version = data.version;
		var beta = data.beta;

		var color = 'white';
		var updatespan = $('AlesaDams_updates_span');
		if (!$chk(updatespan)) {
		    updatespan = new Element('span', {
			id: 'AlesaDams_updates_span',
			// copied from Google++ userscript:
			styles: {
				'background': '#E0E0E0',
				padding: '2px 4px',
				display: 'block',
				'-moz-background-clip': 'border',
				'-moz-background-origin': 'padding',
				'-moz-background-inline-policy': 'continuous',
				position: 'fixed',
				opacity: '0.7',
				'z-index': 1011, // the lightbox layer is 1004 on the new photo page
				bottom: '5px',
				right: '5px',
				left: '5px'
			}
		    }).inject($(document).getElement("body"));
		} else {
			// make it darker if already created by another script:
			updatespan.setStyle('background', '#E0E0E0');
		}
		var bgColor = beta ? 'grey' : 'black';
		var updater = new Element('span', {
			styles: {
				background: bgColor + ' none repeat scroll 0% 0%',
				margin: '2px 5px',
				position: 'relative',
				'float': beta ? 'left' : 'right',
				opacity: beta ? '0.5' : ''
			}
		}).inject(updatespan);
		new Element('a', {
			html: 'New Flickr Post Notification: ' + (beta ? 'beta ' + version : onlineVersion + ' available'),
			href: installPage,
			target: '_blank',
			title: 'to the ' + (beta ? 'stable version' : '') + ' install page (opens in new tab)',
			styles: {
				'color': color,
				'text-decoration': 'none'
			}
		}).inject(updater);
		new Element('a', {
			html: beta ? ' (feedback)' : ' (Changes)',
			title: 'opens in new tab',
			href: 'http://www.flickr.com/groups/flickrhacks/discuss/72157623920770381/lastpage',
			styles: {
				'text-decoration': 'none'
			},
			target: '_blank'
		}).inject(updater);
	}

function getThreadInfo(data) {
        var threadId = data.threadId;
        var callback = data.callback;

        try {  
                new Request({
                        url: 'http://www.flickr.com',
                        onSuccess: function (responseText, responseXML) {
			    try {
				var result;
				try {
				    result = JSON.parse(responseText);
				} catch (e) {
					GM_log("error parsing JSON reply for thread " + threadId + " (" + e + "): '" + responseText);
					callback({ threadId: threadId, success: false, message: e });
					return;
				}
				if (result.stat === 'fail') {
					callback({ threadId: threadId, success: false, message: result.message });
				} else {
					callback({ threadId: threadId, success: true, groupId: result.replies.topic.group_id, message: result.replies.topic.message._content });
				}
			    } catch (e) {
				GM_log("ERROR processing result: " + e);
				callback({ threadId: threadId, success: false, message: "ERROR processing result: " + e });
			    }
			},
                        onFailure: function (response) {
                                GM_log("error: " + response.statusText);
                                callback({ threadId: threadId, success: false, message: response.statusText });
                        }
                }).get('/services/rest', {
                        api_key: GM_getMagisterLudi(),
                        auth_hash: GM_getAuthHash(),
			csrf: GM_getCsrf(),
                        auth_token: GM_getAuthToken(),
                        format: 'json',
                        method: 'flickr.groups.discuss.replies.getList', // flickr.groups.discuss.topics.getInfo does not return group_id
                        nojsoncallback: 1,
                        topic_id: threadId,
			per_page: 1,
			page: 1
                });
        } catch (e) {
                callback({ threadId: threadId, success: false, message: 'Exception: ' + e });
        }
}

function checkVersion(version) {
try {
	var lastVersionCheckTime = GM_getValue("lastVersionCheckTime");
	var elapsedtime;
	var CPStartTime = new Date();
	if (lastVersionCheckTime !== undefined) {
		elapsedtime = CPStartTime.getTime() - lastVersionCheckTime;
	}
	if (!lastVersionCheckTime || elapsedtime / 1000 > 60 * 60 * 12) { //more then 12h ago
		getThreadInfo({ threadId: '72157623920770381', callback: function(retval) {
				var success = retval.success;
				var message = retval.message;
				if (!success) {
					GM_log("error getting version info: " + message);
					GM_deleteValue("onlineVersion");
					GM_deleteValue("lastVersionCheckTime");
					return;
				}
				var onlineVersion = message.split(/<i>current stable version:\s*/)[1].split(/<\/i>/)[0];
				GM_setValue("onlineVersion", onlineVersion);
				GM_setValue("lastVersionCheckTime", (new Date()).getTime().toString());
			}
		});
	}

	var onlineVersion = GM_getValue("onlineVersion");
	if ($chk(onlineVersion)) {
		var updateAvailable = false;
		var reVersionMatch = /(\d+)\.(\d+)\.(\d+)/;
		var onlineVersionParts = reVersionMatch.exec(onlineVersion);
		var currentVersionParts = reVersionMatch.exec(version);
		var onlineVersionMajor, onlineVersionMinor, onlineVersionBuild;
		//[ onlineVersion, onlineVersionMajor, onlineVersionMinor, onlineVersionBuild ] = onlineVersionParts; 'invalid left-hand side' in Chrome
		onlineVersionMajor = onlineVersionParts[1];
		onlineVersionMinor = onlineVersionParts[2];
		onlineVersionBuild = onlineVersionParts[3];
		var currentVersionMajor, currentVersionMinor, currentVersionBuild;
		//[ currentVersion, currentVersionMajor, currentVersionMinor, currentVersionBuild] = currentVersionParts;
		currentVersionMajor = currentVersionParts[1];
		currentVersionMinor = currentVersionParts[2];
		currentVersionBuild = currentVersionParts[3];
		// first check major: important update! => rewrite, flickr updates, greasemonkey updates
		if (parseInt(onlineVersionMajor, 10) > parseInt(currentVersionMajor, 10)) {
			updateAvailable = true;
		} else if (parseInt(onlineVersionMajor, 10) === parseInt(currentVersionMajor, 10)) { // we don't want to downgrade
			// minor version update => new functionality
			if (parseInt(onlineVersionMinor, 10) > parseInt(currentVersionMinor, 10)) {
				updateAvailable = true;
			} else if (parseInt(onlineVersionMinor, 10) === parseInt(currentVersionMinor, 10)) { // we don't want to downgrade
				// build version update => bugfixes
				if (parseInt(onlineVersionBuild, 10) > parseInt(currentVersionBuild, 10)) {
					updateAvailable = true;
				}
			}
		}
		if (updateAvailable) {
			showUpdateNotification({ onlineVersion: onlineVersion });
		} else if (version != onlineVersion) {
			showUpdateNotification({ version: version, beta: true });
		}
	}
} catch(e) {
	GM_log("checkVersion error: " + e);
}
}

// end version check code

var NFPNPreferences = new Class({
	options: {
		gotoLastThreadPage: false, // on the FlickrNotifications envelopes
		gotoLastThreadPageLink: false, // on the Flickr Discuss thread links
		colorizeNewPosts: true,
		scrollToNewPosts: true,
		refreshButtonAsLast: false,
		reloadTimeout: 1 // in hours
	},
	initialize: function() {
		var storedGotoLastThreadPage = GM_getValue("NFPN.preference.gotoLastThreadPage");
		if (storedGotoLastThreadPage !== undefined) {
			this.options.gotoLastThreadPage = (storedGotoLastThreadPage === true || storedGotoLastThreadPage === "true"); // Google chrome trickery
		}
		var storedGotoLastThreadPageLink = GM_getValue("NFPN.preference.gotoLastThreadPageLink");
		if (storedGotoLastThreadPageLink !== undefined) {
			this.options.gotoLastThreadPageLink = (storedGotoLastThreadPageLink === true || storedGotoLastThreadPageLink === "true"); // Google chrome trickery
		}
		var storedColorizeNewPosts = GM_getValue("NFPN.preference.colorizeNewPosts");
		if (storedColorizeNewPosts !== undefined) {
			this.options.colorizeNewPosts = (storedColorizeNewPosts === true || storedColorizeNewPosts === "true");
		}
		var storedScrollToNewPosts = GM_getValue("NFPN.preference.scrollToNewPosts");
		if (storedScrollToNewPosts !== undefined) {
			this.options.scrollToNewPosts = (storedScrollToNewPosts === true || storedScrollToNewPosts === "true");
		}
		var storedRefreshButtonAsLast = GM_getValue("NFPN.preference.refreshButtonAsLast");
		if (storedRefreshButtonAsLast !== undefined) {
			this.options.refreshButtonAsLast = (storedRefreshButtonAsLast === true || storedRefreshButtonAsLast === "true");
		}
		this.options.reloadTimeout = parseInt(GM_getValue("NFPN.preference.reloadTimeout", 1));
	},
	gotoLastThreadPage: function() {
		return this.options.gotoLastThreadPage;
	},
	setGotoLastThreadPage: function(b) {
		if (b !== this.options.gotoLastThreadPage) {
			GM_setValue("NFPN.preference.gotoLastThreadPage", b);
			this.options.gotoLastThreadPage = b;
			return true;
		}
		return false;
	},
	gotoLastThreadPageLink: function() {
		return this.options.gotoLastThreadPageLink;
	},
	setGotoLastThreadPageLink: function(b) {
		if (b !== this.options.gotoLastThreadPageLink) {
			GM_setValue("NFPN.preference.gotoLastThreadPageLink", b);
			this.options.gotoLastThreadPageLink = b;
			return true;
		}
		return false;
	},
	colorizeNewPosts: function() {
		return this.options.colorizeNewPosts;
	},
	setColorizeNewPosts: function(b) {
		if (b !== this.options.colorizeNewPosts) {
			GM_setValue("NFPN.preference.colorizeNewPosts", b);
			this.options.colorizeNewPosts = b;
			return true;
		}
		return false;
	},
	scrollToNewPosts: function() {
		return this.options.scrollToNewPosts;
	},
	setScrollToNewPosts: function(b) {
		if (b !== this.options.scrollToNewPosts) {
			GM_setValue("NFPN.preference.scrollToNewPosts", b);
			this.options.scrollToNewPosts = b;
			return true;
		}
		return false;
	},
	refreshButtonAsLast: function() {
		return this.options.refreshButtonAsLast;
	},
	setRefreshButtonAsLast: function(b) {
		if (b !== this.options.refreshButtonAsLast) {
			GM_setValue("NFPN.preference.refreshButtonAsLast", b);
			this.options.refreshButtonAsLast = b;
			return true;
		}
		return false;
	},
	reloadTimeout: function() {
		var retval = this.options.reloadTimeout;
		try {
			retval = parseInt(retval, 10);
		} catch (e) {
			retval = 1;
		}
		if (retval < 1 || isNaN(retval)) {
			retval = 1;
		}
		return retval;
	},
	setReloadTimeout: function(t) {
		if (t !== this.options.reloadTimeout) {
			GM_setValue("NFPN.preference.reloadTimeout", t);
			this.options.reloadTimeout = t;
			return true;
		}
		return false;
	}
});

var preferencesInitData = {
	photo: {
		type: 'photo', // used in DOM id's => no funny characters!
		gm_keyPostfix: 'Photo', // to create /^selectedPhoto./
		headerTitle: 'Selected photos',
		identifier: 'photoId',
		newitems: 'newComments',
		items: 'comments',
		error: 'error',
		titleTemplate: {
			text: '%1 (by <i>%2</i>)',
			param1: 'title',
			param2: 'username'
		},
		text: 'photo', // to be inserted into 'remove this photo from ...'
		checkForNewCallback: checkForNewCommentsInPhotoPage,
		urlCreator: createPhotoPageURL
	},
	photostream: {
		type: 'photostream',
		gm_keyPostfix: 'Photostream',
		headerTitle: 'Selected photostreams',
		identifier: 'userNsid',
		newitems: 'newPhotos',
		items: 'photos',
		error: 'error',
		text: 'photostream',
		checkForNewCallback: checkForNewPhotosInPhotostream,
		urlCreator: createPhotostreamURL
	},
	thread: {
		type: 'thread',
		gm_keyPostfix: 'Thread',
		headerTitle: 'Selected threads',
		identifier: 'topic',
		newitems: 'newPosts',
		items: 'replies',
		error: 'error',
		titleTemplate: {
			text: '%1 (from group <i>%2</i>)',
			param1: 'title',
			param2: 'groupname'
		},
		text: 'thread',
		checkForNewCallback: apiCheckForNewPostsInThread,
		urlCreator: createThreadURL
	},
	group: {
		type: 'group',
		gm_keyPostfix: 'Group',
		headerTitle: 'Selected group discussion pages',
		identifier: 'groupname',
		newitems: 'newPosts',
		items: 'replies',
		error: 'invalidCount',
		text: 'discussion group',
		checkForNewCallback: apiCheckForNewPostsInGroup,
		urlCreator: createGroupURL
	},
	helpforum: {
		type: 'helpforum',
		gm_keyPostfix: 'Helpforum',
		headerTitle: 'Selected help forums',
		identifier: 'groupname',
		newitems: 'newPosts',
		items: 'replies',
		error: 'invalidCount',
		text: 'help forum',
		titleTemplate: {
			text: '%1 (in <i>%2</i>)',
			param1: 'title',
			param2: 'groupname'
		},
		checkForNewCallback: checkForNewPostsInHelpForum,
		urlCreator: createGroupURL
	},
	forumitem: {
		type: 'forumitem',
		gm_keyPostfix: 'Forumitem',
		headerTitle: 'Selected forum threads',
		identifier: 'topic',
		newitems: 'newPosts',
		items: 'replies',
		error: 'error',
		text: 'forum thread',
		checkForNewCallback: checkForNewPostsInForumThread,
		urlCreator: createThreadURL
	},
	groupmembers: {
		type: 'groupmembers',
		gm_keyPostfix: 'Groupmembers',
		headerTitle: 'Selected group member pages',
		identifier: 'groupId',
		newitems: 'newMembers',
		items: 'members',
		error: 'error',
		text: 'group members',
		checkForNewCallback: checkForNewMembersInGroup,
		urlCreator: createGroupMembersURL
	},
	grouppool: {
		type: 'grouppool',
		gm_keyPostfix: 'Grouppool',
		headerTitle: 'Selected group pools',
		identifier: 'groupId',
		newitems: 'newPhotos',
		items: 'photos',
		error: 'error',
		text: 'group pool',
		checkForNewCallback: checkForNewPhotosInGroupPool,
		urlCreator: createGroupPoolURL
	},
	reversedcontacts: {
		type: 'reversedcontacts',
		gm_keyPostfix: 'Reversedcontacts',
		headerTitle: 'Selected reversed contacts pages',
		identifier: 'userNsid',
		newitems: 'newContacts',
		items: 'members',
		error: 'error',
		text: 'reversed contact',
		checkForNewCallback: checkForNewReversedContacts,
		urlCreator: createReversedContactsURL
	},
	pendingitems: {
		type: 'pendingitems',
		gm_keyPostfix: 'Pendingitems',
		headerTitle: 'Selected pending items pages',
		identifier: 'groupname',
		newitems: 'newItems',
		items: 'items',
		error: 'error',
		text: 'pending items',
		checkForNewCallback: checkForPendingItems,
		urlCreator: createPendingItemsURL
	},
	app: {
		type: 'app',
		gm_keyPostfix: 'App',
		headerTitle: 'Selected applications',
		identifier: 'appId',
		newitems: 'newComments',
		items: 'comments',
		error: 'error',
		titleTemplate: {
			text: '%1 (by <i>%2</i>)',
			param1: 'title',
			param2: 'username'
		},
		text: 'app',
		checkForNewCallback: checkForNewAppComments,
		urlCreator: createAppURL
	},
	set: {
		type: 'set',
		gm_keyPostfix: 'Set',
		headerTitle: 'Selected sets',
		identifier: 'setId',
		newitems: 'newComments',
		items: 'comments',
		error: 'error',
		titleTemplate: {
			text: '%1 (by <i>%2</i>)',
			param1: 'title',
			param2: 'realname'
		},
		text: 'set',
		checkForNewCallback: checkForNewSetComments,
		urlCreator: createSetURL
	},
	pendingtestimonials: {
		type: 'pendingtestimonials',
		gm_keyPostfix: 'Pendingtestimonials',
		headerTitle: 'Pending testimonials',
		identifier: 'userNsid',
		newitems: 'newItems',
		items: 'items',
		error: 'error',
		text: 'pending testimonials',
		checkForNewCallback: checkForPendingTestimonials,
		urlCreator: createPendingTestimonialsURL
	},
	pendingmembers: {
		type: 'pendingmembers',
		gm_keyPostfix: 'Pendingmembers',
		headerTitle: 'Pending group member pages',
		identifier: 'groupId',
		newitems: 'newMembers',
		items: 'members',
		error: 'error',
		text: 'pending members',
		checkForNewCallback: checkForPendingMembers,
		urlCreator: createPendingMembersURL
	},
	favorites: {
		type: 'favorites',
		gm_keyPostfix: 'Favorites',
		headerTitle: 'Favorites',
		identifier: 'userNsid',
		newitems: 'newFavorites',
		items: 'favorites',
		error: 'error',
		text: 'favorites',
		checkForNewCallback: checkForNewUserFavorites,
		urlCreator: createUserFavoritesURL
	}
};

function applyPreferences() {
	// last thread page (envelopes)
	var gotoLastThreadPageCheckbox = $('NFPN.gotolastthreadpage');
	if ($chk(gotoLastThreadPageCheckbox)) {
		var gotoLastPage = $('NFPN.gotolastthreadpage').checked;
		var changed = preferences.setGotoLastThreadPage(gotoLastPage);
		if (changed) { // change links on the fly
			var anchors = [];
			// add/remove 'lastpage' from thread and forumitem urls
			anchors.combine($$('img[id^=NFPN.' + preferencesInitData.thread.gm_keyPostfix + ']').getParent('a'))
				.combine($$('span[id^=NFPN.' + preferencesInitData.thread.gm_keyPostfix + ']').getElement('a'));
			anchors.combine($$('img[id^=NFPN.' + preferencesInitData.forumitem.gm_keyPostfix + ']').getParent('a'))
				.combine($$('span[id^=NFPN.' + preferencesInitData.forumitem.gm_keyPostfix + ']').getElement('a'));
			anchors.each(function(anchor) {
				if ($chk(anchor)) {
					if (gotoLastPage) { // add '/lastpage'
						if (!anchor.get('href').match('lastpage')) anchor.set('href', anchor.get('href') + '/lastpage');
					} else { // remove '/lastpage'
						anchor.set('href', anchor.get('href').replace(/\/lastpage$/, ''));
					}
				}
			});
			// add/remove 'pageX' from photo urls
			anchors = [];
			anchors.combine($$('img[id^=NFPN.' + preferencesInitData.photo.gm_keyPostfix + '\.]').getParent('a'))
				.combine($$('span[id^=NFPN.' + preferencesInitData.photo.gm_keyPostfix + '\.]').getElement('a'));
			anchors.each(function(anchor) {
				if ($chk(anchor)) {
					try {
						var photoid = anchor.get('href').match(/.*flickr.com\/photos\/[^\/]+\/(\d+)/)[1];
						if (gotoLastPage) { // add '/pageX'
							var metaInfo = GM_getObject('selected' + preferencesInitData.photo.gm_keyPostfix + '.' + photoid);
							if (metaInfo.key_present) {
								var photoInfo = metaInfo.value;
								var page = Math.ceil(photoInfo.comments / 50);
								if (page > 1) {
									anchor.set('href', anchor.get('href') + 'page' + page + '/');
								}
							}
						} else { // remove '/pageX'
							anchor.set('href', anchor.get('href').replace(/page\d+\/$/, ''));
						}
					} catch(e) {
						GM_log("error: " + e);
					}
				}
			});
		}
	}

	// last thread page (Flickr Discuss links)
	var gotoLastThreadPageLinkCheckbox = $('NFPN.gotolastthreadpagelink');
	if ($chk(gotoLastThreadPageLinkCheckbox)) {
		var gotoLastPageLink = $('NFPN.gotolastthreadpagelink').checked;
		var changed = preferences.setGotoLastThreadPageLink(gotoLastPageLink);
		if (changed) { // change links on the fly
			if (document.location.href.test(/.*flickr.com\/groups\/[^\/]+\/discuss/)) { // only when on a group Discuss page
				var minusIcon; // only when a watch is defined on the group Discuss page
				minusIcon = $$('img[id^=NFPN.plusmin.icon.Group.]');
				if ($chk(minusIcon)) { // we are definitely on a group Discuss page
					if (('' + minusIcon.get('title')).test(/^remove/)) { // only when already watching
						try {
							getMain().getElement('table.TopicListing').getElements('tr').each(function (row,idx) {
							    if (idx === 0) { return; } // title row
							    if (gotoLastPageLink) {
								if (!row.getElement('a').get('href').match('lastpage'))
									row.getElement('a').set('href', row.getElement('a').get('href') + '/lastpage');
							    } else {
								row.getElement('a').set('href', row.getElement('a').get('href').replace(/\/lastpage$/, ''));
							    }
							});
						} catch (e) {
							GM_log("error updating links: " + e);
						}
					}
				}
			}
		}
	}

	var colorizeNewPostsCheckbox = $('NFPN.colorizenewposts');
	if ($chk(colorizeNewPostsCheckbox)) {
		var colorizeNewPosts = $('NFPN.colorizenewposts').checked;
		preferences.setColorizeNewPosts(colorizeNewPosts);
	}

	var scrollToNewPostsCheckbox = $('NFPN.scrolltonewposts');
	if ($chk(scrollToNewPostsCheckbox)) {
		var scrollToNewPosts = $('NFPN.scrolltonewposts').checked;
		preferences.setScrollToNewPosts(scrollToNewPosts);
	}

	var refreshButtonAsLastCheckbox = $('NFPN.refreshbuttonaslast');
	if ($chk(refreshButtonAsLastCheckbox)) {
		var refreshButtonAsLast = $('NFPN.refreshbuttonaslast').checked;
		preferences.setRefreshButtonAsLast(refreshButtonAsLast);
	}

	var reloadTimeoutInput = $('NFPN.reloadtimeout');
	if ($chk(reloadTimeoutInput)) {
		var reloadTimeout = reloadTimeoutInput.value;
		preferences.setReloadTimeout(reloadTimeout);
	}

	$each(preferencesInitData, function(initData) {
		// removals
		$$('img.NFPN-' + initData.type + '-remove').each(function(imgElement) {
			var id = imgElement.get('id').replace('NFPN.' + initData.type + '.remove.', '');
			// remove from data store
			GM_deleteValue('selected' + initData.gm_keyPostfix + '.' + id);
			// TODO!! if group or helpforum: removeThreadsFromCache
			// remove from screen
			try {
				resetPostsIcon(initData.gm_keyPostfix, id);
				$('NFPN.' + initData.gm_keyPostfix + '.' + id).getParent('a').dispose();
			} catch(e) {
				try { // if the item was only just added, it has no anchor parent (and no messagechunk)
					$('NFPN.' + initData.gm_keyPostfix + '.' + id).dispose();
				} catch(e) {
					GM_log(e);
				}
			}
		});
		// envelope colors
		$$('img.NFPN-' + initData.type + '-bgcolor').each(function(imgElement) {
			var id = imgElement.get('id').replace('NFPN.' + initData.type + '.bgColor.', '');
			var metaInfo = GM_getObject('selected' + initData.gm_keyPostfix + '.' + id);
			if (metaInfo.key_present) { // could have been removed
				var storedInfo = metaInfo.value;
				if (storedInfo) {
					var bgColor = imgElement.getAttribute("nfpncolor");
					if (bgColor !== storedInfo.bgColor) {
						storedInfo.bgColor = bgColor;
						GM_storeObject('selected' + initData.gm_keyPostfix + '.' + id, storedInfo);
						$('NFPN.' + initData.gm_keyPostfix + '.' + id).style.backgroundColor = bgColor;
					}
				} else {
					// nothing to do: should have been reported when opening the preferences dialog
				}
			}
		});
	});

	/*    if (somethingChanged) {
        window.location.reload();
    } else { */
	togglePreferencesDialog();
	/*    } */
}

function createColorCell(html, title, bgColor, selectedColor, colorName) {
	if (colorName === undefined) {
		colorName = bgColor;
	}
	var colorCell = new Element('td', {
		html: html,
		title: title,
		width: 17,
		height: 20,
		align: 'center',
		styles: {
			background: bgColor,
			cursor: 'pointer'
		},
		events: {
			click: function(evt) {
				// remove old checkmark
				this.getParent('table').getElements('img[src=' + images.ok + ']').dispose();
				// set new color
				this.getParent('div').getElement('img.nfpn-colorpicker').setAttribute('nfpncolor', colorName);
				this.getParent('div').getElement('img.nfpn-colorpicker').style.backgroundColor = colorName === 'none' ? '': bgColor;
				// put new checkmark
				new Element('img', { // known issue: click twice on a cell, and there's no checkmark?
					src: images.ok
				}).inject($(evt.target));
			}
		}
	})
	if (selectedColor === colorName) {
		new Element('img', {
			src: images.ok
		}).inject(colorCell);
	}
	return colorCell;
}

function createColorsTable(bgColor) {
	var colorsTable = new Element('table', {
		cellPadding: 0,
		cellSpacing: 1,
		styles: {
			display: 'inline',
		}
	});
	var colorsRow;
	x11Colors.each(function(color, idx) {
		if (idx === 35) {
			/*            colorCell = createColorCell('Flickr', 
                'same as Flickr background',
                $(document).getElement('body').getComputedStyle('background-color'),
                bgColor,
                'Flickr');
            colorCell.rowSpan = 4;
            colorCell.inject(colorsRow); */
			colorCell = createColorCell('none', 'don\'t use a background color', 'white', bgColor, 'none');
			colorCell.rowSpan = 4;
			colorCell.inject(colorsRow);
			if (bgColor === undefined || bgColor === null) {
				colorCell.adopt(new Element('img', {
					src: images.ok
				}));
			}
		}
		if ((idx % 35) === 0) {
			colorsRow = new Element('tr').inject(colorsTable);
		}

		createColorCell(' ', color, color, bgColor).inject(colorsRow);
	});
	return colorsTable;
}

function createRemoveIcon(id, title, removeClass) {
	return new Element('img', {
		src: images.remove,
		id: id,
		title: title,
		styles: {
			cursor: 'pointer'
		},
		events: {
			click: function(evt) {
				this.toggleClass(removeClass);
				if (this.hasClass(removeClass)) {
					this.getParent('div').setStyle('text-decoration', 'line-through');
				} else {
					this.getParent('div').setStyle('text-decoration', '');
				}
			}
		}
	});
}

function createBgColorIcon(id, title, selectedColor, colorClass) {
	return new Element('img', {
		src: images.color,
		id: id,
		title: title,
		styles: {
			cursor: 'pointer',
			backgroundColor: (selectedColor === undefined || selectedColor === 'none' ? '':
					  selectedColor === 'Flickr' ? $(document).getElement('body').getComputedStyle('background-color') :
					  selectedColor)
		},
		'class': 'nfpn-colorpicker ' + colorClass,
		nfpncolor: selectedColor,
		events: {
			click: function(evt) {
				if (this.hasClass('showing')) {
					this.removeClass('showing');
					this.getParent().getElement('span.colortable').destroy();
				} else {
					this.addClass('showing');
					var bgColor = this.getAttribute('nfpncolor');
					var colorsTable = createColorsTable(bgColor);
					colorsTable.inject(new Element('span', {
						'class': 'colortable'
					}).inject(this.getParent('div')));
				}
			}
		}
	});
}

GM_addStyle('div.NFPN-preferences-item { min-width: 300; float: left; display: inline; }');
function togglePreferencesDialog(evt) {
	var preferencesDialog = $('NFPN.preferences');
	if ($chk(preferencesDialog)) {
		preferencesDialog.destroy();
	} else {
		// TODO: keep the Cancel and OK buttons on the screen
		var topline = evt.event.pageY < 50 ? evt.event.pageY: 50;
		var maxWidth = (window.innerWidth - 50 < 800 ? window.innerWidth - 50 : 800);
		var left = (window.innerWidth - maxWidth) / 2;
		var preferencesTable = new Element('div', {
			id: 'NFPN.preferences',
			styles: {
				overflow: 'auto',
				background: '#BFBFBF',
				'-moz-border-radius': '4px',
				'-webkit-border-radius': '4px',
				'-khtml-border-radius': '4px',
				'border-radius': '4px',
				border: 'grey solid 1px',
				padding: '2px 4px',
				zIndex: 1014,
				maxHeight: (window.innerHeight - topline - 20),
				maxWidth: maxWidth,
				minWidth: 640,
				position: 'fixed',
				opacity: '0.98',
				left: left + 'px', //evt.event.pageX,
				top: topline,
				display: 'block',
				visibility: 'visible',
				fontSize: '12px'
			}
		}).inject($(document).getElement('body'));
GM_addStyle('div.NFPN-preferences-comment { font-style: italic; float: left; max-width: ' + (preferencesTable.getStyle('maxWidth').match(/(\d+)/)[1] - 350) + 'px; vertical-align: top; padding-left: 10px; }');
		var gotoLastThreadPageCheckbox;
		var colorizeNewPostsCheckbox;
		var scrollToNewPostsCheckbox;
		var refreshButtonAsLastCheckbox;
		var reloadTimeoutInput;
		var header = new Element('div').inject(preferencesTable);
		new Element('span', {
                        title: 'consider a donation',
                        html: 'donate:',
                        styles: {
                                'float': 'left',
                                'font-style': 'italic',
                                'font-size': 'smaller',
                                'vertical-align': 'middle'
                        }
                }).adopt(
                        new Element('a', {
                                href: 'https://www.paypal.com/be/cgi-bin/webscr?cmd=_send-money&email=alesadam@ymail.com&amount_ccode=USD&payment_type=S',
                                title: 'donate with Paypal',
                                target: '_blank'
                        }).adopt(
                                new Element('img', { src: images.paypal, styles: { 'vertical-align': 'middle' } })
                        ),
                        new Element('a', {
                                href: 'http://www.amazon.co.uk/registry/wishlist/RWKOXV8NS09I/',
                                title: 'buy me something from my Amazon wishlist (UK)',
                                target: '_blank'
                        }).adopt(
                                new Element('img', { src: images.amazon, styles: { 'vertical-align': 'middle' } })
                        ),
                        new Element('a', {
                                href: 'http://www.amazon.com/gp/wishlist/2FWKY0A7NVSOJ/',
                                title: 'buy me something from my Amazon wishlist (US)',
                                target: '_blank'
                        }).adopt(
                                new Element('img', { src: images.amazon, styles: { 'vertical-align': 'middle' } })
                        ),
                        new Element('a', {
                                href: 'javascript:void(O);',
                                title: 'donate Bitcoins',
                                events: {
                                        click: function (evt) {
                                                // based on http://leo.bogert.de/2012/02/22/simple-bitcoin-donate-button/
                                                window.prompt ('Please copy-paste my Bitcoin address to your Bitcoin software - I cannot do it automatically.\nTo copy it, right-click the selected text and select \'Copy\'.\nThen right-click the address field in your Bitcoin software and select \'Paste\'.', '19DvtjDgiSx9991xVkXSYSovsSoreSGDC7');
                                        }
                                }
                        }).adopt(
                                new Element('img', { src: images.bitcoin, styles: { 'vertical-align': 'middle' } })
                        )
                ).inject(header);
		new Element('label', {
			id: 'NFPN.preferences_title',
			html: 'Flickr Notifications - Preferences',
			styles: {
				fontWeight: 'bold',
				width: (preferencesTable.getStyle('maxWidth').match(/(\d+)/)[1] - 50) + 'px',
				'float': 'center'
			},
			events: {
				'version': function(evt) {
					this.set('html', 'Flickr Notifications v' + evt.version + ' - Preferences');
				}
			}
		}).inject(header);
		getVersion('NFPN.preferences_title', 'version');
		new Element('a', {
			html: 'feedback thread',
			href: 'http://www.flickr.com/groups/flickrhacks/discuss/72157623920770381/',
			title: 'opens in new tab',
			target: '_blank',
			styles: {
				'float': 'right'
			}
		}).inject(header);

		new Element('hr').inject(preferencesTable);

		// go to last page (envelope)
		var gotoLastPagePreference = new Element('div', {
			align: 'left',
			styles: {
				clear: 'both'
				//float: left
			}
		}).inject(preferencesTable);
		gotoLastPagePreference.adopt(
		    new Element('div', {
			'class': 'NFPN-preferences-item'
		    }).adopt(
			gotoLastThreadPageCheckbox = new Element('input', {
				type: 'checkbox',
				id: 'NFPN.gotolastthreadpage'
			}),
			new Element('label', {
				'for': 'NFPN.gotolastthreadpage',
				html: 'go to the thread\'s last page (envelope)'
			})
		    ),
		    new Element('div', {
			'class': 'NFPN-preferences-comment',
			html: "The link on the envelopes take you to the first page of the thread. " +
				"<br>With this option set, the link will take you to the the last page of the thread." +
				"<br>Applicable for discussion threads, forum items, and photo pages (not group " +
				"<br>discussion pages, or group pools, ..)"
		    })
		);
		if (preferences.gotoLastThreadPage()) {
			gotoLastThreadPageCheckbox.set('checked', 'checked');
		}
		// go to last page (Flickr links)
		var gotoLastPageLinkPreference = new Element('div', {
			align: 'left',
			styles: {
				clear: 'both'
				//float: left
			}
		}).inject(preferencesTable);
		gotoLastPageLinkPreference.adopt(
		    new Element('div', {
			'class': 'NFPN-preferences-item'
		    }).adopt(
			new Element('input', {
				type: 'checkbox',
				id: 'NFPN.gotolastthreadpagelink'
			}),
			new Element('label', {
				'for': 'NFPN.gotolastthreadpagelink',
				html: 'go to the thread\'s last page (Flickr link)'
			})
		    ),
		    new Element('div', {
			'class': 'NFPN-preferences-comment',
			html: "For multipage threads, Flickr provides a link to the last page of that thread in the 'Latest' reply column " +
				"of the Discuss page. The links in the 'Title' column take you to the thread\'s first page" +
				"<br>With this option set, the link in the 'Title' column will take you to the the last page of the thread."
		    })
		);
		if (preferences.gotoLastThreadPageLink()) {
			$('NFPN.gotolastthreadpagelink').set('checked', 'checked');
		}

		var colorizeNewPostsPreference = new Element('div', {
			align: 'left',
			styles: {
				clear: 'both'
			}
		}).inject(preferencesTable);
		colorizeNewPostsPreference.adopt(
		    new Element('div', {
			'class': 'NFPN-preferences-item'
		    }).adopt(
		    	colorizeNewPostsCheckbox = new Element('input', {
				type: 'checkbox',
				id: 'NFPN.colorizenewposts'
		    	}),
		    	new Element('label', {
				'for': 'NFPN.colorizenewposts',
				html: 'colorize new posts'
		    	})
		    ),
		    new Element('div', {
			'class': 'NFPN-preferences-comment',
			html: "Colorizes new posts to easily find them." +
				"<br>Applicable for discussion threads, help forum items and photo pages."
		    })
		);
		if (preferences.colorizeNewPosts()) {
			colorizeNewPostsCheckbox.set('checked', 'checked');
		}

		var scrollToNewPostsPreference = new Element('div', {
			align: 'left',
			styles: {
				clear: 'both'
			}
		}).inject(preferencesTable);
		scrollToNewPostsPreference.adopt(
		    new Element('div', {
			'class': 'NFPN-preferences-item'
		    }).adopt(
			scrollToNewPostsCheckbox = new Element('input', {
				type: 'checkbox',
				id: 'NFPN.scrolltonewposts'
			}),
			new Element('label', {
				'for': 'NFPN.scrolltonewposts',
				html: 'scroll down to new posts'
			})
		    ),
		    new Element('div', {
			'class': 'NFPN-preferences-comment',
			html: "Scrolls a page down to the new posts." +
				"<br>Applicable for discussion threads and help forum items (and very dependent on script order)."
		    })
		);
		if (preferences.scrollToNewPosts()) {
			scrollToNewPostsCheckbox.set('checked', 'checked');
		}

		var refreshButtonAsLastPreference = new Element('div', {
			align: 'left',
			styles: {
				clear: 'both'
			}
		}).inject(preferencesTable);
		refreshButtonAsLastPreference.adopt(
		    new Element('div', {
			'class': 'NFPN-preferences-item'
		    }).adopt(
			refreshButtonAsLastCheckbox = new Element('input', {
				type: 'checkbox',
				id: 'NFPN.refreshbuttonaslast'
			}),
			new Element('label', {
				'for': 'NFPN.refreshbuttonaslast',
				html: 'move refresh button to the right'
			})
		    ),
		    new Element('div', {
			'class': 'NFPN-preferences-comment',
			html: "Moves the 'refresh' button to the right of the envelopes."
		    })
		);
		if (preferences.refreshButtonAsLast()) {
			refreshButtonAsLastCheckbox.set('checked', 'checked');
		}

		var reloadTimeoutPreference = new Element('div', {
			align: 'left',
			styles: {
				clear: 'both'
			}
		}).inject(preferencesTable);
		reloadTimeoutPreference.adopt(
		    new Element('div', {
			'class': 'NFPN-preferences-item'
		    }).adopt(
			new Element('label', {
				html: 'Reload timeout: '
			}),
			reloadTimeoutInput = new Element('input', {
				type: 'number',
				id: 'NFPN.reloadtimeout',
				value: preferences.reloadTimeout(),
				styles: {
					width: '40px'
				}
			}),
			new Element('label', {
				html: 'h'
			})
		    ),
		    new Element('div', {
			'class': 'NFPN-preferences-comment',
			html: "The reload button will change from light grey to red in the time specified (in hours)." +
				"After this timeout, the reload button will start to blink."
		    })
		);
		var keyValues = GM_listKeys();
		//keyValues.sort();
		$each(preferencesInitData, function(initData) {
			var headerPrinted = false;
			var reKey = new RegExp('^selected' + initData.gm_keyPostfix + '\\.');
			$each(keyValues, function(key) {
				if (key.test(reKey)) {
					var metaInfo = GM_getObject(key);
					if (metaInfo.key_present) { // it always is
						var storedInfo = metaInfo.value;
						if ($chk(storedInfo)) {
							if (!headerPrinted) {
								headerPrinted = true;
								var header = new Element('div', {
									html: initData.headerTitle + ' ',
									styles: {
										background: '#CFCFCF',
										textAlign: 'left',
										clear: 'both'
									}
								}).inject(preferencesTable);
								header.adopt(
								    new Element('img', {
									src: images.down,
									title: 'click to expand',
									styles: {
										height: 12,
										cursor: 'pointer'
									},
									events: {
										click: function(evt) {
											if (evt.target.title.match(/expand/)) {
												evt.target.src = images.up;
												evt.target.title = 'click to collapse';
												$$('div.NFPN-preference-' + initData.gm_keyPostfix).each(function(pref) {
													pref.style.display = 'block';
													pref.style.visibility = 'visible';
												});
											} else {
												evt.target.src = images.down;
												evt.target.title = 'click to expand';
												$$('div.NFPN-preference-' + initData.gm_keyPostfix).each(function(pref) {
													pref.style.display = 'none';
													pref.style.visibility = 'hidden';
												});
											}
										}
									}
								    })
								);
							} // headerPrinted
							var title = storedInfo.title + ' ';
							if ($chk(initData.titleTemplate)) {
								title = initData.titleTemplate.text
									.replace('%1', storedInfo[initData.titleTemplate.param1])
									.replace('%2', storedInfo[initData.titleTemplate.param2]) 
									+ ' ';
							}
							var itemPreferences = new Element('div', {
								'class': 'NFPN-preference-' + initData.gm_keyPostfix,
								styles: {
									textAlign: 'left',
									display: 'none',
									visibility: 'hidden',
									clear: 'both'
								}
							}).inject(preferencesTable);
							itemPreferences.adopt(
							    createRemoveIcon(
							    	'NFPN.' + initData.type + '.remove.' + storedInfo[initData.identifier],
							    	'remove this ' + initData.text + ' from your watch list',
								'NFPN-' + initData.type + '-remove'),
							    new Element('span', {
								html: ' '
							    }),
							    createBgColorIcon(
							    	'NFPN.' + initData.type + '.bgColor.' + storedInfo[initData.identifier],
								'choose a background color for this ' + initData.text + '\'s envelope',
								storedInfo.bgColor,
								'NFPN-' + initData.type + '-bgcolor'),
							    new Element('span', {
								html: ' '
							    }),
							    new Element('span', {
								html: title
							    })
							);
						} else {
							// would like to report missing object, but
							// missing too much information
						}
					}
				}
			});
		});

		preferencesTable.adopt(
		    new Element('div', {
			align: 'right'
		    }).adopt(
		    	new Element('button', {
				type: 'submit',
				html: 'OK',
				'class': 'Butt',
				events: {
					click: applyPreferences
				}
		    	}),
		    	document.createTextNode(' '),
		    	new Element('button', {
				type: 'submit',
				html: 'Cancel',
				'class': 'DeleteButt',
				events: {
					click: togglePreferencesDialog
				}
		    	})
		    )
		);

		preferencesTable.adopt(
			new Element('div', {
				styles: {
					'text-align': 'left'
				}
			}).adopt(
				new Element('img', {
					src: images.convert,
					id: 'NFPN.consistency.img',
					styles: {
						cursor: 'pointer'
					},
					events: {
						click: function (evt) {
							this.set('src', images.updating);
							var div = $('NFPN.consistency-problems');
							div.empty();
							keyValues = GM_listKeys('.*', function(key) {
								var listKey = false;
								var processed = false;
								var forced = false;
								if (key.toString().match('Object')) {
									listKey = true;
									processed = true;
								}
								// thread info for a selected group
								if (!processed && key.match(/.+\.http:\/\/www\.flickr\.com\/groups\/[^\/]+\/discuss\/\d+/)) {
									var groupname = key.match(/^(.+)\.http:\/\/www\.flickr\.com\/groups\/[^\/]+\/discuss\/\d+/)[1];
									listKey = !GM_getObject('selectedGroup.' + groupname).key_present;
									processed = true;
								}
								// same for selected forum
								if (!processed && key.match(/.+\.http:\/\/www\.flickr\.com\/help\/forum\/[^\/]+\/\d+/)) {
									var forumname = key.match(/^(.+)\.http:\/\/www\.flickr\.com\/help\/forum\/[^\/]+\/\d+/)[1];
									listKey = !GM_getObject('selectedHelpforum.' + forumname).key_present;
									processed = true;
								}
								// no longer used:
								if (!processed && (key.match(/^\w+\.lastChecked$/) || 
										 key.match(/^[\w\.]+\.(defaultC|c)heckDelays?$/) ||
										 key.match("NFPN.preference.compactEnvelopeStyle")
										)) {
									listKey = true;
									forced = true;
									processed = true;
								}
								// known, and used preferences:
								if (!processed && (key.match(/^NFPN\.preference\.colorizeNewPosts$/) ||
										   key.match(/^NFPN\.preference\.scrollToNewPosts$/) ||
										   key.match(/^NFPN\.preference\.gotoLastThreadPage$/) ||
										   key.match(/^NFPN\.preference\.gotoLastThreadPageLink$/) ||
										   key.match(/^NFPN\.preference\.refreshButtonAsLast$/) ||
										   key.match(/^NFPN\.preference\.reloadTimeout$/) ||
										   key.match(/^lastReloadTime$/) ||
										   key.match(/^lastVersionCheckTime$/) ||
										   key.match(/^onlineVersion$/) ||
										   key.match(/^version$/)
										)) {
									processed = true;
								}
								// known, and used selections:
								if (!processed && key.match(/^selected.+\..+/)) {
									processed = true;
									var gm_keyPostfix = key.match(/^selected(.+)\..+$/)[1];
									var initData = preferencesInitData[gm_keyPostfix.toLowerCase()];
									if (!$chk(initData)) {
										listKey = true;
										forced = true;
									} else if (initData.gm_keyPostfix != gm_keyPostfix) {
										listKey = true;
										forced = true;
									}
								}
								if (listKey || !processed) {
									div.adopt(
										new Element('br'),
										new Element('input', {
											type: 'checkbox',
											checked: forced,
											id: 'NFPN.consistency.' + key,
										}),
										new Element('label', {
											'for': 'NFPN.consistency.' + key,
											id: 'NFPN.consistency.' + key + '-label',
											html: key.toString()
										})
									);
								};
							});
							if ($$('input[id^=NFPN.consistency]').length == 0) {
								this.set('src', images.ok);
								return;
							}
							this.set('src', images.convert);
							new Element('button', {
								type: 'submit',
								html: 'Remove selected keys',
								'class': 'DeleteButt',
								styles: {
									display: 'block'
								},
								events: {
									click: function(evt) {
										$$('input[id^=NFPN.consistency]:checked').each(function (checkbox) {
											var key = checkbox.get('id').replace('NFPN.consistency.', '');
											GM_deleteValue(key);
											$(checkbox.get('id') + '-label').setStyle('text-decoration', 'line-through');
										});
									}
								}
							}).inject(div);
						}
					}
				}),
				new Element('span', {
					html: 'check data consistency',
					styles: {
						'padding-left': '5px',
						cursor: 'pointer'
					},
					events: {
						click: function (evt) {
							$('NFPN.consistency.img').fireEvent('click');
						}
					}
				})
			),
			new Element('div', {
				id: 'NFPN.consistency-problems',
				styles: {
					'text-align': 'left'
				}
			})
		);
	}
}

function addPreferencesMenuItem() {
	if (!$chk($('NFPN.envelopes'))) {
		var injectionPoint = $('global-nav');
		if (!$chk(injectionPoint) && document.location.href.match(/.*flickr.com\/photos\/[^\/]+\/\d+/)) {
			// create a global-nav on a photo page
			injectionPoint = new Element('div', {
				styles: {
					'background': '#E0E0E0',
					padding: '2px 4px',
					display: 'block',
					'-moz-background-clip': 'border',
					'-moz-background-origin': 'padding',
					'-moz-background-inline-policy': 'continuous',
					position: 'fixed',
					opacity: '0.9',
					'z-index': 1011, // the lightbox layer is 1004 on the new photo page
					top: '5px',
					right: '320px',
					left: '5px'
				}
			}).inject($$('body')[0]);
		}
		var injectionDirection = 'bottom';
		new Element('div', {
			id: 'NFPN.envelopes',
			styles: {
				'font-size': '10px ! important',
				'text-align': 'center',
				'background': 'rgba(150,150,150,0.8)',
				'margin-right': '5px'
			}
		}).inject(injectionPoint, injectionDirection);
	}
// removed from Organize menu; using an icon now
	new Element('img', {
		title: 'Flickr Notifications Preferences',
		id: 'NFPN.preferences.icon',
		src: images.preferences,
		styles: {
			cursor: 'pointer',
			padding: '4px',
			'vertical-align': 'middle'
		},
		events: {
			click: function(evt) {
				togglePreferencesDialog(evt);
			}
		}
	}).inject($('NFPN.envelopes'), 'top');
// add reload button now
	new Element('img', {
		title: 'Check for new items',
		id: 'NFPN.reload.icon',
		src: images.reload,
		styles: {
			cursor: 'pointer',
			padding: '4px',
			'vertical-align': 'middle',
		},
		events: {
			click: function (evt) {
				GM_log("reloading...");
				GM_setValue("lastReloadTime", (new Date()).getTime().toString());
				$$('img.NFPN-envelope-img').each(function(envelope) {
					if (!envelope.hasClass('needs-conversion')) {
						envelope.fireEvent('start-spinning');
					}
				});
				$$('img.NFPN-envelope-img').each(function(envelope) {
					envelope.fireEvent('run-check');
				});
				this.fireEvent('update-color');
			},
			'update-color': function (evt) {
				var lastReloadTime = GM_getValue("lastReloadTime");
				if (!$chk(lastReloadTime)) {
					lastReloadTime = new Date().getTime();
					GM_setValue("lastReloadTime", lastReloadTime.toString());
				}
				lastReloadTime = parseInt(lastReloadTime, 10);
				var now = new Date().getTime();
				var timeoutInterval = 1000 * 60 * 60 * preferences.reloadTimeout();
				var timePast = now - lastReloadTime;
				if (timePast >= timeoutInterval) { // start blinking
					if (!$chk(blinking)) {
						blinking = setInterval(blinkReloadButton, 1000);
						this.addClass('blinking-off');
					}
					return;
				}
				// if past this point, make sure to stop blinking
				if ($chk(blinking)) {
					clearInterval(blinking);
					blinking = undefined;
				}
				this.removeClass('blinking-on');
				this.removeClass('blinking-off');
				var colorIdx = parseInt((timePast / timeoutInterval) * whiteToRedGradient.length, 10);
				if (colorIdx >= whiteToRedGradient.length) {
					colorIdx = whiteToRedGradient.length - 1;
				}
				this.setStyle('background-color', whiteToRedGradient[colorIdx]);
			},
			'blink-again': function (evt) {
				if (this.hasClass('blinking-on')) {
					this.removeClass('blinking-on');
					this.addClass('blinking-off');
					this.setStyle('background-color', whiteToRedGradient[whiteToRedGradient.length - 1]);
				} else if (this.hasClass('blinking-off')) {
					this.addClass('blinking-on');
					this.removeClass('blinking-off');
					this.setStyle('background-color', whiteToRedGradient[0]);
				}
			}
		}
	}).inject($('NFPN.preferences.icon'), 'after');
	// change color depending on how long it has been since the last refresh
	updateReloadButtonColor();
	var intervalTimer = parseInt(preferences.reloadTimeout() * 60 * 60 * 1000 / whiteToRedGradient.length, 10);
	intervalTimer = parseInt(intervalTimer / 3, 10); // make it run smooth enough
	setInterval(updateReloadButtonColor, intervalTimer);
}

var whiteToRedGradient = [
	// in 16 shades of red:
	'#F0F0F0', '#F1E0E0', '#F2D0D0', '#F3C0C0', '#F4B0B0', '#F5A0A0', '#F69090', '#F78080', '#F87070', '#F96060', '#FA5050', '#FB4040', '#FC3030', '#FD2020', '#FE1010', '#FF0000'
];

var blinking = undefined;
function updateReloadButtonColor() {
	$('NFPN.reload.icon').fireEvent('update-color');
}
function blinkReloadButton() {
	$('NFPN.reload.icon').fireEvent('blink-again');
}

function retrieveThreadsFromCache(groupname) {
	var retval = new Hash();
	var obsolete = new Array(); // do not delete while iterating: creates havoc!
	var reMatch = new RegExp("^" + groupname + "\\.");
	GM_listKeys(reMatch, function (key) {
		// inspect the key
		if (key.toString().match('Object')) { // typeof won't do it; key.match() neither
			// old bug => will result in double keys in data => 'ghost' new-replies
			GM_log("removing Object key '" + key + "'");
			obsolete.push(key);
			return;
		}
		var metaInfo = GM_getObject(key);
		if (metaInfo.key_present) { // always true :-)
			var threadInfo = metaInfo.value;
			if (threadInfo) {
				retval.set(threadInfo.topic, threadInfo);
			} else {
				GM_log("removing key '" + key + "'");
				obsolete.push(key); // clean up
			}
		}
	});
	obsolete.forEach(function (key) {
		GM_log("removing key '" + key + "' from storage");
		GM_deleteValue(key);
	});
	return retval;
}

function resolveUsername(userNsid, callback) {
	new Request({
		url: 'http://www.flickr.com',
		onSuccess: function(responseText, responseXML) {
			var result = JSON.parse(responseText);
			if (result.stat === 'fail') {
				GM_log("error reading user information: " + result.message);
				callback({ success: false, message: "error reading user information: " + result.message });
				return;
			}
			callback({ success: true, displayname: result.person.username._content, userpath: result.person.path_alias });
		},
		onFailure: function(response) {
			GM_log("failure reading user information: " + response.statusText);
			callback({ success: false, message: "failure reading user information: " + response.statusText });
		}
	}).get('/services/rest', {
		api_key: GM_getMagisterLudi(),
		auth_hash: GM_getAuthHash(),
			csrf: GM_getCsrf(),
		auth_token: GM_getAuthToken(),
		format: 'json',
		method: 'flickr.people.getInfo',
		nojsoncallback: 1,
		user_id: userNsid
	});
}

function getMain(element) {
	if (!$chk(element)) {
		element = document;
	}
	var main = element.getElement('*[id=Main]');
	if (!$chk(main)) main = element.getElement('*[id=main]');
	if (!$chk(main)) main = element.getElement('*[id=meta]');
	return main;
}

GM_addStyle(".f-sprite { background-image: url('http://l.yimg.com/g/images/flickr-sprite.png.v4'); }\n " +
	            ".fs-icon_unread_bw { background-position: -812px -13px; }\n " + 
		    ".fs-icon_unread { background-position: -812px -53px; }");

GM_addStyle("a.NFPN-envelope:hover { background: #F0F0F0; }");
GM_addStyle("a.NFPN-envelope-hover { font-weight: bold; line-height: 10px; }");
GM_addStyle('.NFPN_hint { background-color: #FFE28A; }');

function createPostsIcon(data) {
	var prefix = data.prefix;
	var id = data.id;
	var url = data.url;
	var title = data.title;
	var bgColor = data.bgColor;
	var isStored = data.isStored;

	try {
		var postsAnchor = new Element('a', {
			id: 'NFPN.' + prefix + '.' + id + '.MessageAnchor',
			'class': 'NFPN-envelope',
			href: url,
			events: {
				mouseover: function (evt) {
					this.addClass('NFPN-envelope-hover');
				},
				mouseout: function (evt) {
					this.removeClass('NFPN-envelope-hover');
				}
			},
			styles: {
				position: 'relative'
			}
		}).inject(preferences.refreshButtonAsLast() ? $('NFPN.reload.icon') : $('NFPN.envelopes'),
		          preferences.refreshButtonAsLast() ? 'before' : 'bottom');

		var newPostsIcon = new Element('img', {
			'class': 'NFPN-envelope-img f-sprite fs-icon_unread_bw',
			id: 'NFPN.' + prefix + '.' + id,
			src: images.updating,
			title: title,
			alt: title, // also serves as a backup for resetPostsIcon
			height: 10,
			styles: {
				'font-size': '9px ! important',
				'background-color': (bgColor === undefined || bgColor === 'none' ?
					'' : bgColor === 'Flickr' ?
					$(document).getElement('body').getComputedStyle('background-color') :
					bgColor),
				'vertical-align': 'middle',
				width: '16px'
			},
			events: {
				'reset-count': function (evt) {
				    try {
					var compactMessageChunk = $(this.id + '.compactMessageChunk');
					if ($chk(compactMessageChunk)) {
						compactMessageChunk.destroy();
					}
					this.src = images.spaceout;
					//this.set('class', 'f-sprite fs-icon_unread_bw');
					this.removeClass('fs-icon_unread');
					this.addClass('fs-icon_unread_bw');
					this.set('height', 16);
					this.set('title', this.get('alt'));
				    } catch (e) {
					GM_log("error resetting envelope count: " + e);
				    }
				},
				'set-count': function (evt) {
				    try {
					var compactMessageChunk = $(this.id + '.compactMessageChunk');
					if (evt.newPosts > 0) {
						if ($chk(compactMessageChunk)) {
							compactMessageChunk.empty();
						} else {
							var anchor = this.getParent('a');
							compactMessageChunk = new Element('span', {
								id: this.id + '.compactMessageChunk',
								'class': 'NFPN-compact-message-chunk'
							}).inject(anchor);
						}
						this.src = images.spaceout;
						//this.set('class', 'f-sprite fs-icon_unread');
						this.removeClass('fs-icon_unread_bw');
						this.addClass('fs-icon_unread');
						this.set('height', 16);
						compactMessageChunk.set('html', evt.newPosts + (evt.returnedThreads === 'true' ? '+': ''));
						compactMessageChunk.set('title', this.get('alt') + ' (' + evt.newPosts + (evt.returnedThreads === 'true' ? '+': '') + ')');
						this.set('title', compactMessageChunk.get('title'));
					} else {
						this.fireEvent('reset-count');
					}
				    } catch (e) {
					GM_log("error setting envelope count: " + e);
				    }
				},
				'show-error': function (evt) {
					try {
						this.src = images.error;
						this.height = 16;
						this.title = this.title + " - " + evt;
					} catch(e) {
						GM_log("error notifying error: " + e + " (error was: " + evt + ")");
					}
				},
				'needs-conversion': function (evt) {
					this.src = images.convert;
					this.height = 16;
					this.title = this.title + " - click to convert to new version";
					this.addClass('needs-conversion');
				},
				'mark-locked': function (evt) {
					try {
						this.src = images.lock;
						this.height = 16;
						this.title = this.get('title') + " - (locked)";
					} catch(e) {
						GM_log("error showing lock: " + e);
					}
				},
				'start-spinning': function (evt) {
					this.src = images.updating;
					this.height = 10;
				},
				'run-check': function (evt) {
					if (this.hasClass('needs-conversion')) return;
					this.fireEvent('reset-count');
					var key =  this.id.replace('NFPN.','selected');
					var metaInfo = GM_getObject(key);
					if (!metaInfo.key_present) { // sanity check
						this.fireEvent('show-error', 'INTERNAL ERROR: created an envelope for a non-existing key!?');
						return;
					}
					var storedInfo = metaInfo.value;
					if (!$chk(storedInfo)) {
						this.fireEvent('show-error', 'INTERNAL ERROR: missing data!?');
						return;
					}
					this.setStyle('background-color', storedInfo.bgColor);
					var gm_keyPostfix = key.match(/selected([^\.]+)\..*$/)[1];
					var initData = preferencesInitData[gm_keyPostfix.toLowerCase()];
					if (!$chk(initData)) {
						this.fireEvent('show-error', 'INTERNAL ERROR: missing initData for keyPostfix=\'' + gm_keyPostfix + '\'');
						return;
					}
					this.fireEvent('start-spinning');
					try {
						initData.checkForNewCallback(storedInfo, this);
					} catch(e) {
						this.fireEvent('show-error', e);
					}
				}
			}
		}).inject(postsAnchor);

		if (!isStored) return newPostsIcon; // called from the + icon

		var key =  'selected' + prefix + '.' + id;
		var metaInfo = GM_getObject(key);
		if (!metaInfo.key_present) { // sanity check
			newPostsIcon.fireEvent('show-error', 'INTERNAL ERROR: created an envelope for a non-existing key!?');
			return newPostsIcon;
		}
		var storedInfo = metaInfo.value;
		if (!$chk(storedInfo)) {
			newPostsIcon.fireEvent('show-error', 'INTERNAL ERROR: missing data!?');
			return newPostsIcon;
		}
		var initData = preferencesInitData[prefix.toLowerCase()];
		if (!$chk(initData)) {
			newPostsIcon.fireEvent('show-error', 'INTERNAL ERROR: missing initData for keyPostfix=\'' + gm_keyPostfix + '\'');
			return newPostsIcon;
		}
		var title = prefix + ': ' + storedInfo.title + ' ';
		if ($chk(initData.titleTemplate)) {
			title = prefix + ': ' + initData.titleTemplate.text
				.replace('%1', storedInfo[initData.titleTemplate.param1])
				.replace('%2', storedInfo[initData.titleTemplate.param2])
				.replace(/<[^>]+>/g, '') // remove HTML markup
				+ ' ';
		}
		newPostsIcon.set('title', title);
		newPostsIcon.set('alt', title);

		return newPostsIcon;
	} catch(e) {
		GM_log("error creating mail icon");
	}
}

function resetPostsIcon(gm_keyPostfix, id) {
	var newPostsIcon = $('NFPN.' + gm_keyPostfix + '.' + id);
	if ($chk(newPostsIcon)) {
		newPostsIcon.fireEvent('reset-count');
	}
	return newPostsIcon;
}

GM_addStyle('span.NFPN-compact-message-chunk { z-index: 2; position: absolute; right: 0; top: 0; font-size: 10px; color: black; }');

function notifyNewPosts(prefix, id, newPosts, returnedThreads) {
	var newPostsIcon = $('NFPN.' + prefix + '.' + id);
	if ($chk(newPostsIcon)) {
		newPostsIcon.fireEvent('set-count', { newPosts: newPosts, returnedThreads: returnedThreads });
	}
}

function notifyError(prefix, id, title) {
	var newPostsIcon = $('NFPN.' + prefix + '.' + id);
	if ($chk(newPostsIcon)) {
		newPostsIcon.fireEvent('show-error', title);
	}
}

function markThreadLocked(prefix, id) {
	var newPostsIcon = $('NFPN.' + prefix + '.' + id);
	if ($chk(newPostsIcon)) {
		newPostsIcon.fireEvent('mark-locked');
	}
}

function spinNewPostsIcon(prefix, id) {
	var newPostsIcon = $('NFPN.' + prefix + '.' + id);
	if ($chk(newPostsIcon)) {
		newPostsIcon.fireEvent('start-spinning');
	}
}

function reportMissingObject(data) {
	var type = data.type;
	var url = data.url;
	var key = data.key;
	var html = data.html ? data.html: url;

	if ($chk($('NFPN.envelopes'))) {
		var pinky = new Element('div', {
			styles: {
				backgroundColor: 'pink'
			},
			events: {
				'update-link': function (evt) {
					this.getElement('a').set('href', evt.url);
					this.getElement('a').set('html', evt.html ? evt.html : evt.url);
				}
			}
		}).inject($('NFPN.envelopes'), 'after');
		pinky.adopt(
		    new Element('span', {
			html: 'Error! found a key for a ' + type + ' for '
		    }),
		    new Element('a', {
			href: url,
			html: html ? html: '&lt;missing&gt;'
		    }),
		    new Element('span', {
			html: ' but no valid data '
		    }),
		    new Element('a', {
			href: '#',
			html: '(remove key)',
			events: {
				click: function(evt) {
					evt.preventDefault();
					GM_deleteValue(key);
					$(evt.target).getParent('div').dispose();
				}
			}
		    })
		);
		return pinky;

	} else {
		GM_log("error: no envelopes element for error reporting");
	}
}

function createGroupURL(groupInfo) {
	var helpforum = groupInfo.helpforum;
	var gm_keyPostfix = (helpforum ? "Helpforum": "Group");
	var groupDiscussionURL = (helpforum ?
		"http://www.flickr.com/help/forum/" + groupInfo.groupname + "/" :
		"http://www.flickr.com/groups/" + groupInfo.groupname + "/discuss/");
	return { matchURL: groupDiscussionURL, linkURL: groupDiscussionURL };
}

function checkForNewPostsInHelpForum(forumInfo, newPostsIcon) {
	forumInfo.helpforum = true;
	var helpforum = forumInfo.helpforum;
	var gm_keyPostfix = "Helpforum";
	var groupDiscussionUrl = createGroupURL(forumInfo).matchURL;

	var debug = false; //forumInfo.groupname == 'central';
	if (debug) GM_log(["DEBUG: checking posts in forum " + forumInfo.groupname,
				"id=" + forumInfo[preferencesInitData.group.identifier],
				"URL=" + groupDiscussionUrl,
				"storedInfo.items=" + forumInfo['replies'],
				"storedInfo.newItems=" + forumInfo['newPosts']
			].join('\n'));
	newPostsIcon.getParent('a').set('href', groupDiscussionUrl);
	var data = {
		debug: debug,
		storedInfo: forumInfo,
		initData: preferencesInitData.helpforum,
		URL: groupDiscussionUrl,
		countItemsInPage: function(element, callback) {
			var storedThreads = retrieveThreadsFromCache(forumInfo.groupname);
			var groupNewPosts = 0;
			var returnedThreads = 'false';
			element.getElements('table.TopicListing').each(function(table) { // help forums use two tables
				table.getElements('tr').each(function(row, index) {
					if (index === 0) {
						return; // header
					}
					var columns = row.getElements('td');
					var threadAnchor = columns[0].getElement('a');
					var title = threadAnchor.textContent;
					var topic = threadAnchor.href;
					var replies = parseInt(columns[2].textContent.replace(/(\.|,)/g, ''), 10);
					var threadInfo = storedThreads.get(topic);
					if ($chk(threadInfo)) {
						if (threadInfo.returnedThread === 'true') {
							groupNewPosts += 1;
							returnedThreads = 'true';

						} else if (threadInfo.newTopic === 'true') {
							groupNewPosts += replies;
							groupNewPosts++; // also count the topic starter
							threadInfo.newPosts = replies + 1;
							threadInfo.replies = replies;
							GM_storeObject(forumInfo.groupname + '.' + topic, threadInfo);

						} else {
							if (threadInfo.replies < replies) {
								threadInfo.newPosts = (replies - threadInfo.replies);
								// don't store replies to keep a clean record
								groupNewPosts += threadInfo.newPosts;
								GM_storeObject(forumInfo.groupname + '.' + topic, threadInfo);

							} else if (replies < threadInfo.replies) { // someone deleted a comment; record
								returnedThreads = 'true';
								threadInfo.deletedItems = 'true';
								threadInfo.replies = replies; // reset - has no effect
								groupNewPosts++;
								GM_storeObject(forumInfo.groupname + '.' + topic, threadInfo);
							}
						}
						// do not attempt to update the envelope of this thread if it has its own envelope
					} else {
						// unknown in local storage, means we haven't seen it the last time
						// could be a thread that returned from pageX
						// or it could be a completely new topic
						// if we read the RSS feed, and it contains all the posts of this thread, it's new
						// (rss feed only contains 10 entries; if posts in thread > 10 => we can't check)
						// if there are no replies yet, it's also new
						if (replies === 0) { // new topic
							groupNewPosts++; // thread start
							// store as new: if it gets some replies, it would be considered a returned thread
							GM_storeObject(forumInfo.groupname + '.' + topic, {
								groupname: forumInfo.groupname,
								topic: topic,
								newPosts: replies + 1,
								replies: replies,
								title: title,
								newTopic: 'true'
							});
						} else {
							returnedThreads = 'true';
							groupNewPosts++;
						}
					}
				});
			});
			callback({ success: true, items: groupNewPosts, returnedThreads: returnedThreads });
		},
		updateInfo: function(element, forumInfo) {
			forumInfo.title = element.getElement('h1[id=Tertiary]').textContent.replace(/\n[^]*$/im, '');
		}
	};
	newSomethingHtmlCall(data);
}

function apiCheckForNewPostsInGroup(groupInfo, newPostsIcon) {
	var gm_keyPostfix = preferencesInitData.group.gm_keyPostfix;
	var groupDiscussionUrl = createGroupURL(groupInfo).matchURL;

	var debug = false; // groupInfo.groupname == 'canondslr';
	if (debug) GM_log(["DEBUG: checking posts in group " + groupInfo.groupname,
				"id=" + groupInfo[preferencesInitData.group.identifier],
				"URL=" + groupDiscussionUrl,
				"storedInfo.items=" + groupInfo['replies'],
				"storedInfo.newItems=" + groupInfo['newPosts']
			].join('\n'));
	newPostsIcon.getParent('a').set('href', groupDiscussionUrl);

	newSomethingApiCall({
		initData: preferencesInitData.group,
		storedInfo: groupInfo,
		itemsExtractor: function(result, groupInfo, callback) {
			var storedThreads = retrieveThreadsFromCache(groupInfo.groupname);
			var groupNewPosts = 0;
			var returnedThreads = 'false';
			result.topics.topic.each(function(topic) {
				var title = topic.subject;
				var replies = parseInt(topic.count_replies, 10);
				var topicId = topic.id;
				var topicUrl = 'http://www.flickr.com/groups/' + groupInfo.groupname + '/discuss/' + topicId + '/';
				var threadInfo = storedThreads.get(topicUrl);
				/*storedThreads.getKeys().some(function (key) {
					if (key.match(topicId)) {
						threadInfo = storedThreads.get(key);
						return true;
					}
					return false;
				}); */
				if ($chk(threadInfo)) {
					if (threadInfo.returnedThread === 'true') {
						groupNewPosts += 1;
						returnedThreads = 'true';

					} else if (threadInfo.newTopic === 'true') {
						groupNewPosts += replies;
						groupNewPosts++; // also count the topic starter
						threadInfo.newPosts = replies + 1;
						threadInfo.replies = replies;
						GM_storeObject(groupInfo.groupname + '.' + topicUrl, threadInfo);

					} else {
						if (threadInfo.replies < replies) {
							threadInfo.newPosts = (replies - threadInfo.replies);
							// don't store replies to keep a clean record
							groupNewPosts += threadInfo.newPosts;
							GM_storeObject(groupInfo.groupname + '.' + topicUrl, threadInfo);

						} else if (replies < threadInfo.replies) { // someone deleted a comment; record
							returnedThreads = 'true';
							threadInfo.deletedItems = 'true';
							threadInfo.replies = replies; // reset - has no effect
							groupNewPosts++;
							GM_storeObject(groupInfo.groupname + '.' + topicUrl, threadInfo);
						}
					}
					// do not attempt to update the envelope of this thread if it has its own envelope
				} else {
					// unknown in local storage, means we haven't seen it the last time
					// could be a thread that returned from pageX
					// or it could be a completely new topic
					// if we read the RSS feed, and it contains all the posts of this thread, it's new
					// (rss feed only contains 10 entries; if posts in thread > 10 => we can't check)
					// if there are no replies yet, it's also new
					if (replies === 0) { // new topic
						groupNewPosts++; // thread start
						// store as new: if it gets some replies, it would be considered a returned thread
						GM_storeObject(groupInfo.groupname + '.' + topicUrl, {
							groupname: groupInfo.groupname,
							topic: topicUrl,
							newPosts: replies + 1,
							replies: replies,
							title: title,
							newTopic: 'true'
						});
					} else {
						returnedThreads = 'true';
						groupNewPosts++;
					}
				}
			});
			callback({ success: true, items: groupNewPosts, returnedThreads: returnedThreads });
		},
		updateInfo: function(result, groupInfo) {
			var title = result.topics.name;
			groupInfo.title = title;
		},
		apiData: {
			api_key: GM_getMagisterLudi(),
			auth_hash: GM_getAuthHash(),
			csrf: GM_getCsrf(),
			auth_token: GM_getAuthToken(),
			format: 'json',
			method: 'flickr.groups.discuss.topics.getList',
			nojsoncallback: 1,
			group_id: groupInfo.groupId,
			page: 1,
			per_page: 20
		}
	});
}

function createThreadURL(threadInfo) {
	var helpforum = threadInfo.helpforum;
	var gm_keyPostfix = (helpforum ? preferencesInitData.forumitem.gm_keyPostfix : preferencesInitData.thread.gm_keyPostfix);
	var threadURL = (helpforum ?
		"http://www.flickr.com/help/forum/" + threadInfo.groupname + "/" + threadInfo.topic + "/" :
		"http://www.flickr.com/groups/" + threadInfo.groupname + "/discuss/" + threadInfo.topic + "/");
	return { matchURL: threadURL, linkURL: threadURL + (preferences.gotoLastThreadPage() ? '/lastpage' : '') };
}

function checkForNewPostsInForumThread(forumItemInfo, newPostsIcon) {
// BUG:
// watch the group
// watch a thread
// reload..
// it looks like the group's check is right, while the thread check is missing a new reply
// .. or it could be a deleted spam
	forumItemInfo.helpforum = true;
	var threadURL = createThreadURL(forumItemInfo).matchURL;

	var data = {
		storedInfo: forumItemInfo,
		initData: preferencesInitData.forumitem,
		URL: threadURL,
		countItemsInPage: countPostsInThreadPage,
		updateInfo: function(element, forumItemInfo) {
			var title = readThreadTitleFromPage(element);
			var groupTitle = readGroupTitleFromPage(element);

			forumItemInfo.groupTitle = groupTitle;
			// title may have been changed
			forumItemInfo.title = title;
			forumItemInfo.locked = false;
			// closed?
			try {
				var focus = element.getElement('p.Locked');
				if ($chk(focus)) forumItemInfo.locked = true;
			} catch(e) {
				GM_log("error checking lock: " + e);
			}
		},
		updateIcon: function(forumItemInfo) {
			try {
				if (forumItemInfo.locked) markThreadLocked(preferencesInitData.forumitem.gm_keyPostfix, forumItemInfo.topic);
			} catch(e) {
				GM_log("error marking lock: " + e);
			}
		}
	};
	newSomethingHtmlCall(data);
}

function apiCheckForNewPostsInThread(threadInfo, newPostsIcon) {
// BUG:
// watch the group
// watch a thread
// reload..
// it looks like the group's check is right, while the thread check is missing a new reply
// .. or it could be a deleted spam
	var threadURL = createThreadURL(threadInfo).matchURL;

	//if (threadInfo.locked) markThreadLocked(preferencesInitData.thread.gm_keyPostfix, threadInfo.topic);
	if (!$chk(threadInfo.groupId)) {
		// first fill in the missing groupId v4.0.3, and prior
		getThreadInfo({ threadId: threadInfo.topic, callback: function(retval) {
				var success = retval.success;
				var message = retval.message;
				if (!success) {
					GM_log("error getting thread info: " + message);
					threadInfo.error = true;
					threadInfo.errorMsg = message;
					GM_storeObject('selected' + preferencesInitData.thread.gm_keyPostfix + '.' + threadInfo[preferencesInitData.thread.identifier], threadInfo);
					notifyError(preferencesInitData.thread.gm_keyPostfix, threadInfo.topic, message);
					return;
				}
				threadInfo.groupId = retval.groupId;
				GM_storeObject('selected' + preferencesInitData.thread.gm_keyPostfix + '.' + threadInfo[preferencesInitData.thread.identifier], threadInfo);
				apiCheckForNewPostsInThread(threadInfo, newPostsIcon);
			}
		});
	} else {
		newSomethingApiCall({
			initData: preferencesInitData.thread,
			storedInfo: threadInfo,
			itemsExtractor: function(result, threadInfo, callback) {
				callback({ success: true, items: $chk(result.replies) && $chk(result.replies.topic) && $chk(result.replies.topic.total) ? result.replies.topic.total : 0 });
			},
			updateInfo: function(result, threadInfo) {
				var title = result.replies.topic.subject;
				// TODO: update group name; can be done async
				threadInfo.title = title;
				threadInfo.groupId = result.replies.topic.group_id;
				threadInfo.groupTitle = result.replies.topic.name;
				threadInfo.locked = result.replies.topic.is_locked == 1 || result.replies.topic.is_locked == '1';
				threadInfo.datelastpost = result.replies.topic.datelastpost;
			},
			updateIcon: function(threadInfo) {
				try {
					if (threadInfo.locked) markThreadLocked(preferencesInitData.thread.gm_keyPostfix, threadInfo.topic);
				} catch(e) {
					GM_log("error marking lock: " + e);
				}
			},
			apiData: {
				api_key: GM_getMagisterLudi(),
				auth_hash: GM_getAuthHash(),
			csrf: GM_getCsrf(),
				auth_token: GM_getAuthToken(),
				format: 'json',
				method: 'flickr.groups.discuss.replies.getList',
				nojsoncallback: 1,
				topic_id: threadInfo.topic,
				group_id: threadInfo.groupId,
				page: 1,
				per_page: 1
			}
		});
	}
}

function newSomethingHtmlCall(data) {
	var debug = data.debug;
	var URL = data.URL;
	var storedInfo = data.storedInfo;
	var initData = data.initData;
	var countItemsInPage = data.countItemsInPage;
	var updateInfo = data.updateInfo;
	var updateIcon = $chk(data.updateIcon) ? data.updateIcon: function() {};

	storedInfo[initData.error] = true;
	GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);

    try {
	new Request({
		url: URL,
		onSuccess: function(responseText, responseXML) {
			try {
				var tempDiv = new Element('div', {
					html: responseText.stripScripts()
				});
				var problem = tempDiv.getElement('p.Problem');
				if ($chk(problem)) {
					GM_log("error reading " + initData.items + " in " + initData.type + ": " + problem.get('html'));
					storedInfo.error = true;
					storedInfo.errorMsg = problem.get('html');
					GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);
					notifyError(initData.gm_keyPostfix, storedInfo[initData.identifier], problem.get('html'));
					return;
				}
				// common with ApiCall
				countItemsInPage(tempDiv, function(countResult) {
					if (!countResult.success) {
						storedInfo.error = true;
						storedInfo.errorMsg = countResult.message;
						GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);
						notifyError(initData.gm_keyPostfix, storedInfo[initData.identifier], countResult.message);
						return;
					}
					storedInfo.error = false;
					var items = countResult.items;

					var firstTime = initData.type != 'group' && storedInfo[initData.items] === undefined;
					var newsAvailable = firstTime ? 0: (items - storedInfo[initData.items]);
					if (items < storedInfo[initData.items]) { // removed items
						storedInfo[initData.items] = items;
					}
					if (initData.type == 'group' || initData.type == 'helpforum') {
						newsAvailable = items;
					} else {
						newsAvailable = items - storedInfo[initData.items];
					}

					if (!countResult.pending) {
						// don't overwrite the replies!
						storedInfo[initData.newitems] = newsAvailable;
					} else {
						storedInfo[initData.newitems] = items;
						storedInfo[initData.items] = 0;
						newsAvailable = items; // show all as new, for pending
					}
					storedInfo[initData.error] = false;
					updateInfo(tempDiv, storedInfo);

					GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);
					notifyNewPosts(initData.gm_keyPostfix, storedInfo[initData.identifier], newsAvailable, countResult.returnedThreads);
					updateIcon(storedInfo);
				});
			} catch(e) {
				GM_log("exception in newSomethingHtmlCall: " + e);
				storedInfo.error = true;
				storedInfo.errorMsg = "exception in newSomethingHtmlCall: " + e;
				GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);
				notifyError(initData.gm_keyPostfix, storedInfo[initData.identifier], "exception in newSomethingHtmlCall: " + e);
			}
		},
		onFailure: function(response) {
			try {
				GM_log("reading " + initData.gm_keyPostfix + "." + storedInfo[initData.identifier] + " failed: " + response.statusText);
				storedInfo.error = true;
				storedInfo.errorMsg = response.statusText;
				GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);
				notifyError(initData.gm_keyPostfix, storedInfo[initData.identifier], response.statusText);
				// if UNSENT or OPEN, statusText could be empty
			} catch(e) {
				GM_log("error reading " + initData.type + ": " + e);
				notifyError(initData.gm_keyPostfix, storedInfo[initData.identifier], "failed: " + e);
			}
		},
		onException: function(headerName, value) {
			GM_log("exception!!!");
		},
		onCancel: function () {
			GM_log("canceled!!!");
		},
		onTimeout: function () {
			GM_log("timed out!!!");
		}
	}).get();
    } catch (e) {
	GM_log("newSomethingHtmlCall failed: " + e);
	storedInfo.error = true;
	storedInfo.errorMsg = "Failed: " + e;
	GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);
    }
}

function newSomethingApiCall(data) {
	var initData = data.initData;
	var storedInfo = data.storedInfo;
	var itemsExtractor = data.itemsExtractor;
	var updateInfo = data.updateInfo;
	var updateIcon = $chk(data.updateIcon) ? data.updateIcon: function() {}

	storedInfo[initData.error] = true;
	GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);

	new Request({
		url: 'http://www.flickr.com',
		onSuccess: function(responseText, responseXML) {
			try {
				var result;
				try {
					result = JSON.parse(responseText);
				} catch(e) {
					result = eval('(' + responseText + ')');
				}
				if (result.stat === 'fail') {
					GM_log("error reading " + initData.gm_keyPostfix + " page: " + result.message);
					storedInfo.error = true;
					storedInfo.errorMsg = "error reading " + initData.gm_keyPostfix + " page: " + result.message;
					GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);
					notifyError(initData.gm_keyPostfix, storedInfo[initData.identifier], result.message);
					return;
				}
				// common with HtmlCall
				itemsExtractor(result, storedInfo, function(countResult) {
					if (!countResult.success) {
						storedInfo.error = true;
						storedInfo.errorMsg = countResult.message;
						GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);
						notifyError(initData.gm_keyPostfix, storedInfo[initData.identifier], countResult.message);
						return;
					}
					storedInfo.error = false;
					var items = countResult.items;

					var firstTime = initData.type != 'group' && storedInfo[initData.items] === undefined;
					var newsAvailable = firstTime ? 0: (items - storedInfo[initData.items]);
					if (items < storedInfo[initData.items]) { // removed items
						storedInfo[initData.items] = items;
					}
					if (initData.type == 'group' || initData.type == 'helpforum') {
						if (storedInfo[initData.items] == undefined) { // cleanup the data; not used, though
							storedInfo[initData.items] = 0;
						}
						newsAvailable = items;
					} else {
						newsAvailable = items - storedInfo[initData.items];
					}

					if (!countResult.pending) {
						// don't overwrite comments!, but remember the new comments
						storedInfo[initData.newitems] = newsAvailable;
					} else {
						storedInfo[initData.newitems] = items;
						storedInfo[initData.items] = 0;
						newsAvailable = items; // show all as new, for pending
					}
					storedInfo[initData.error] = false;

					updateInfo(result, storedInfo);

					GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);
					notifyNewPosts(initData.gm_keyPostfix, storedInfo[initData.identifier], newsAvailable, countResult.returnedThreads);
					updateIcon(storedInfo);
				});
			} catch(e) {
				GM_log("exception in newSomethingApiCall: " + e);
				storedInfo.error = true;
				storedInfo.errorMsg = "exception in newSomethingApiCall: " + e;
				GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);
				notifyError(initData.gm_keyPostfix, storedInfo[initData.identifier], "exception in newSomethingApiCall: " + e);
			}
		},
		onFailure: function(response) {
			try {
				GM_log("reading " + initData.gm_keyPostfix + "." + storedInfo[initData.identifier] + " failed: " + response.statusText);
				storedInfo.error = true;
				storedInfo.errorMsg = "reading failed: " + response.statusText;
				GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);
				notifyError(initData.gm_keyPostfix, storedInfo[initData.identifier], "reading " + initData.gm_keyPostfix + "." + storedInfo[initData.identifier] + " failed: " + response.statusText);
			} catch(e) {
				GM_log("error reading " + initData.gm_keyPostfix + ": " + e);
				storedInfo.error = true;
				storedInfo.errorMsg = "error reading: " + e;
				GM_storeObject('selected' + initData.gm_keyPostfix + '.' + storedInfo[initData.identifier], storedInfo);
				notifyError(initData.gm_keyPostfix, storedInfo[initData.identifier], "error reading " + initData.gm_keyPostfix + "." + storedInfo[initData.identifier] + ": " + e);
			}
		}
	}).get('/services/rest', data.apiData);
}

function createPhotoPageURL(photoInfo) {
	var photoPageURL = "http://www.flickr.com/photos/" + 
		(photoInfo.ownerId ? photoInfo.ownerId: photoInfo.username) + "/" + photoInfo.photoId + "/";
	var linkURL = photoPageURL;
	if (preferences.gotoLastThreadPage()) {
		var page = Math.ceil(photoInfo.comments / 50);
		if (page > 1) {
			linkURL = photoPageURL + 'page' + page + '/';
		}
	}
	return { matchURL: photoPageURL, linkURL: linkURL };
}

function checkForNewCommentsInPhotoPage(photoInfo, newPostsIcon) {

	newSomethingApiCall({
		initData: preferencesInitData.photo,
		storedInfo: photoInfo,
		itemsExtractor: function(result, photoInfo, callback) {
			callback({ success: true, items: $chk(result.photo) && $chk(result.photo.comments) ? result.photo.comments._content : 0 });
		},
		updateInfo: function(result, photoInfo) {
			// title can have been changed
			photoInfo.title = result.photo.title._content;
			// store possibly empty owner id
			photoInfo.ownerId = result.photo.owner.nsid;
			// username may have been changed
			photoInfo.username = result.photo.owner.username;
		},
		apiData: {
			api_key: GM_getMagisterLudi(),
			auth_hash: GM_getAuthHash(),
			csrf: GM_getCsrf(),
			auth_token: GM_getAuthToken(),
			format: 'json',
			method: 'flickr.photos.getInfo',
			nojsoncallback: 1,
			photo_id: photoInfo.photoId
		}
	});
}

function createPhotostreamURL(streamInfo) {
	var streamURL = "http://www.flickr.com/photos/" + streamInfo.username + "/";
	return { matchURL: streamURL, linkURL: streamURL };
}

function checkForNewPhotosInPhotostream(streamInfo, newPostsIcon) {

	newSomethingApiCall({
		initData: preferencesInitData.photostream,
		storedInfo: streamInfo,
		itemsExtractor: function(result, streamInfo,  callback) {
			callback({ success: true, items: $chk(result.person) && $chk(result.person.photos) && $chk(result.person.photos.count) ? result.person.photos.count._content : 0 });
		},
		updateInfo: function(result, streamInfo) {
			// display name can have been changed
			if (result.person.realname) {
				streamInfo.title = result.person.realname._content;
			}
			if (!$chk(streamInfo.title)) {
				streamInfo.title = result.person.username._content;
			}
			if (!$chk(streamInfo.title)) {
				streamInfo.title = streamInfo.username;
			}
		},
		apiData: {
			api_key: GM_getMagisterLudi(),
			auth_hash: GM_getAuthHash(),
			csrf: GM_getCsrf(),
			auth_token: GM_getAuthToken(),
			format: 'json',
			method: 'flickr.people.getInfo',
			nojsoncallback: 1,
			user_id: streamInfo.userNsid
		}
	});
}

function createGroupMembersURL(groupmembersInfo) {
	var membersURL = "http://www.flickr.com/groups_members.gne?id=" + groupmembersInfo.groupId;
	var matchURL = "http:\\/\\/www.flickr.com\\/groups_members.gne\\?id=" + groupmembersInfo.groupId;
	if ($chk(groupmembersInfo.groupname)) {
		matchURL = matchURL + "|" + "http:\\/\\/www.flickr.com\\/groups\\/" + groupmembersInfo.groupname + "\\/members\\/";
	}
	return { matchURL: matchURL, linkURL: membersURL };
}

function checkForNewMembersInGroup(groupmembersInfo, newPostsIcon) {

	newSomethingApiCall({
		initData: preferencesInitData.groupmembers,
		storedInfo: groupmembersInfo,
		itemsExtractor: function(result, groupmembersInfo, callback) {
			callback({ success: true, items: $chk(result.members) ? result.members.total : 0 });
		},
		updateInfo: function(result, groupmembersInfo) {
			// group title info not available in API result
			// updating group title on processGroupMembersPage
		},
		apiData: {
			api_key: GM_getMagisterLudi(),
			auth_hash: GM_getAuthHash(),
			csrf: GM_getCsrf(),
			auth_token: GM_getAuthToken(),
			format: 'json',
			method: 'flickr.groups.members.getList',
			nojsoncallback: 1,
			group_id: groupmembersInfo.groupId,
			//membertypes: 2, count all members: a new member could already have been promoted
			per_page: 1
		}
	});
}

function createGroupPoolURL(grouppoolInfo) {
	var poolURL = "http://www.flickr.com/groups/" + grouppoolInfo.groupname + "/pool/";
	return { matchURL: poolURL, linkURL: poolURL };
}

function checkForNewPhotosInGroupPool(grouppoolInfo, newPostsIcon) {

	newSomethingApiCall({
		initData: preferencesInitData.grouppool,
		storedInfo: grouppoolInfo,
		itemsExtractor: function(result, grouppoolInfo, callback) {
			callback({ success: true, items: $chk(result.photos) ? result.photos.total : 0 });
		},
		updateInfo: function(result, grouppoolInfo) {
			// group title not available in API result
			// updating title in processGroupPoolPage
		},
		apiData: {
			api_key: GM_getMagisterLudi(),
			auth_hash: GM_getAuthHash(),
			csrf: GM_getCsrf(),
			auth_token: GM_getAuthToken(),
			format: 'json',
			method: 'flickr.groups.pools.getPhotos',
			per_page: 1,
			nojsoncallback: 1,
			group_id: grouppoolInfo.groupId
		}
	});
}

function createReversedContactsURL(reversedContactsInfo) {
	var URL = "http://www.flickr.com/people/" + reversedContactsInfo.username + "/contacts/rev/";
	return { matchURL: URL, linkURL: URL };
}

function checkForNewReversedContacts(reversedContactsInfo, newPostsIcon) {
	var URL = createReversedContactsURL(reversedContactsInfo).matchURL;

	newSomethingHtmlCall({
		storedInfo: reversedContactsInfo,
		initData: preferencesInitData.reversedcontacts,
		URL: URL,
		countItemsInPage: countReversedContacts,
		updateInfo: function(element, reversedContactsInfo) {
		}
	});
}

function checkForPendingSomething(pendingSomethingInfo, callData) {
	var initData = callData.initData;
	var URL = callData.URL;
	var title = callData.title;
	var countPendingSomethingFunction = callData.countItems;
	var updatePendingSomethingInfoFunction = callData.updateInfo;

	newSomethingHtmlCall({
		storedInfo: pendingSomethingInfo,
		initData: initData,
		URL: URL,
		countItemsInPage: countPendingSomethingFunction,
		updateInfo: function(element, pendingSomethingInfo) {
			return updatePendingSomethingInfoFunction(element, pendingSomethingInfo);
		}
	});
}

function createPendingItemsURL(pendingItemsInfo) {
	var URL = "http://www.flickr.com/groups/" + pendingItemsInfo.groupname + "/admin/pending/";
	return { matchURL: URL, linkURL: URL };
}

function checkForPendingItems(pendingItemsInfo, newPostsIcon) {
	var title = 'Pending items: ' + pendingItemsInfo.title;
	var URL = createPendingItemsURL(pendingItemsInfo).matchURL;

	checkForPendingSomething(pendingItemsInfo, {
		initData: preferencesInitData.pendingitems,
		URL: URL,
		title: title,
		countItems: countPendingItems,
		updateInfo: function(element, pendingItemsInfo) {
			pendingItemsInfo.title = readGroupTitleFromPage(element);
		}
	});
}

function createPendingTestimonialsURL(pendingTestimonialsInfo) {
	var URL = "http://www.flickr.com/testimonials_manage.gne";
	return { matchURL: URL, linkURL: URL };
}

function checkForPendingTestimonials(pendingTestimonialsInfo, newPostsIcon) {
	var title = 'Pending testimonials';
	var URL = createPendingTestimonialsURL(pendingTestimonialsInfo).matchURL;

	checkForPendingSomething(pendingTestimonialsInfo, {
		initData: preferencesInitData.pendingtestimonials,
		URL: URL,
		title: title,
		countItems: countPendingTestimonials,
		updateInfo: function(element, pendingTestimonialsInfo) {
		}
	});
}

function createPendingMembersURL(pendingMembersInfo) {
	var URL = "http://www.flickr.com/groups_pending.gne?id=" + pendingMembersInfo.groupId;
	return { matchURL: URL, linkURL: URL };
}

function checkForPendingMembers(pendingMembersInfo, newPostsIcon) {
	var title = 'Pending members: ' + pendingMembersInfo.title;
	var URL = createPendingMembersURL(pendingMembersInfo).matchURL;
	
	checkForPendingSomething(pendingMembersInfo, {
		initData: preferencesInitData.pendingmembers,
		URL: URL,
		title: title,
		countItems: countPendingMembers,
		updateInfo: function(element, pendingMembersInfo) {
			var title = element.getElement('*[id=Tertiary]').getElements('a')[1].get('text');
			pendingMembersInfo.title = title;
		}
	});
}

function createAppURL(appInfo) {
	var URL = "http://www.flickr.com/services/apps/" + appInfo.appId + "/";
	return { matchURL: URL, linkURL: URL };
}

function checkForNewAppComments(appInfo, newPostsIcon) {
	var URL = createAppURL(appInfo).matchURL;
	
	newSomethingHtmlCall({
		storedInfo: appInfo,
		initData: preferencesInitData.app,
		URL: URL,
		countItemsInPage: countCommentsInAppPage,
		updateInfo: function(element, appInfo) {
			var title = element.getElement('h1').get('text');
			var username = element.getElement('*[id=ag-owner-attr]').getElement('b').get('html');
			appInfo.title = title;
			appInfo.username = username;
		}
	});
}

function createSetURL(setInfo) {
	var setURL = "http://www.flickr.com/photos/" + setInfo.username + "/sets/" + setInfo.setId + "/comments/";
	return { matchURL: setURL, linkURL: setURL };
}

function checkForNewSetComments(setInfo, newPostsIcon) {

	newSomethingApiCall({
		initData: preferencesInitData.set,
		storedInfo: setInfo,
		itemsExtractor: function(result, setInfo, callback) {
			callback({ success: true, items: ($chk(result.comments) && $chk(result.comments.comment) ? result.comments.comment.length : 0) });
		},
		updateInfo: function(result, setInfo) {
			// no set name, or username in API result
			// updating info on processSetPage
		},
		apiData: {
			api_key: GM_getMagisterLudi(),
			auth_hash: GM_getAuthHash(),
			csrf: GM_getCsrf(),
			auth_token: GM_getAuthToken(),
			format: 'json',
			method: 'flickr.photosets.comments.getList',
			per_page: 500,
			nojsoncallback: 1,
			photoset_id: setInfo.setId
		}
	});
	/* 'Set: ' + setInfo.title + '(by ' + setInfo.realname + ')', */
}

function createUserFavoritesURL(userInfo) {
	var favoritesURL = "http://www.flickr.com/photos/" + userInfo.userpath + "/favorites/";
	return { matchURL: favoritesURL, linkURL: favoritesURL };
}

function checkForNewUserFavorites(userInfo, newPostsIcon) {
	
	newSomethingApiCall({
		initData: preferencesInitData.favorites,
		storedInfo: userInfo,
		itemsExtractor: function(result, userInfo, callback) {
			callback({ success: true, items: $chk(result.photos) ? result.photos.total : 0 });
		},
		updateInfo: function(result, userInfo) {
			// no info in API result that we can use
		},
		apiData: {
			api_key: GM_getMagisterLudi(),
			auth_hash: GM_getAuthHash(),
			csrf: GM_getCsrf(),
			auth_token: GM_getAuthToken(),
			format: 'json',
			method: 'flickr.favorites.getList',
			per_page: 1,
			nojsoncallback: 1,
			user_id: userInfo.userNsid
		}
	});
}

var watchTypes = {
	group: {
		add: 'add this group\'s Discuss page to the Flickr Notifications watchlist',
		remove: 'remove this group\'s Discuss page from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.group.gm_keyPostfix,
		discussPage: true,
		id: undefined // groupname
	},
	helpforum: {
		add: 'add this help forum to the Flickr Notifications watchlist',
		remove: 'remove this help forum from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.helpforum.gm_keyPostfix,
		helpForum: true,
		id: undefined // groupname
	},
	thread: {
		add: 'add this thread to the Flickr Notifications watchlist',
		remove: 'remove this thread from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.thread.gm_keyPostfix,
		discussThread: true,
		id: undefined // topic
	},
	forumitem: {
		add: 'add this help item to the Flickr Notifications watchlist',
		remove: 'remove this help item from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.forumitem.gm_keyPostfix,
		forumItem: true,
		id: undefined // topic
	},
	photo: {
		add: 'add this photo to the Flickr Notifications watchlist',
		remove: 'remove this photo from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.photo.gm_keyPostfix,
		photoPage: true,
		id: undefined // photoId
	},
	photostream: {
		add: 'add this photostream to the Flickr Notifications watchlist',
		remove: 'remove this photostream from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.photostream.gm_keyPostfix,
		photoStream: true,
		id: undefined // username
	},
	groupmembers: {
		add: 'add this group\'s members page to the Flickr Notifications watchlist',
		remove: 'remove this group\'s members page from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.groupmembers.gm_keyPostfix,
		groupmembers: true,
		id: undefined // groupId
	},
	grouppool: {
		add: 'add this group\'s pool page to the Flickr Notifications watchlist',
		remove: 'remove this group\'s pool page from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.grouppool.gm_keyPostfix,
		grouppool: true,
		id: undefined // groupId
	},
	reversedcontacts: {
		add: 'add this reversed contacts page to the Flickr Notifications watchlist',
		remove: 'remove this reversed contacts page from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.reversedcontacts.gm_keyPostfix,
		reversedcontacts: true,
		id: undefined // usernsid
	},
	pendingitems: {
		add: 'add this group\'s pending items page to the Flickr Notifications watchlist',
		remove: 'remove this group\'s pending items page from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.pendingitems.gm_keyPostfix,
		pendingitems: true,
		id: undefined // groupname
	},
	app: {
		add: 'add this application page to the Flickr Notifications watchlist',
		remove: 'remove this application page from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.app.gm_keyPostfix,
		app: true,
		id: undefined // application id
	},
	set: {
		add: 'add this set to the Flickr Notifications watchlist',
		remove: 'remove this set from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.set.gm_keyPostfix,
		set: true,
		id: undefined // set id
	},
	pendingtestimonials: {
		add: 'add this testimonials page to the Flickr Notifications watchlist',
		remove: 'remove this testimonials page from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.pendingtestimonials.gm_keyPostfix,
		pendingtestimonials: true,
		id: undefined // usernsid
	},
	pendingmembers: {
		add: 'add this group\'s pending members page to the Flickr Notifications watchlist',
		remove: 'remove this group\'s pending members page from the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.pendingmembers.gm_keyPostfix,
		pendingmembers: true,
		id: undefined // group id
	},
	favorites: {
		add: 'add this user\'s favorites page to the Flickr Notifications watchlist',
		gm_keyPostfix: preferencesInitData.favorites.gm_keyPostfix,
		favorites: true,
		id: undefined // usernsid
	}
};

function addMinusIcon(watchType) {
	if ($chk($('NFPN.plusmin.icon.' + watchType.gm_keyPostfix + '.' + watchType.id))) { // already there
		return $('NFPN.plusmin.icon.' + watchType.gm_keyPostfix + '.' + watchType.id);
	}

	var icon = new Element('img', {
		src: images.minus,
		id: 'NFPN.plusmin.icon.' + watchType.gm_keyPostfix + '.' + watchType.id,
		height: '10px',
		styles: {
			cursor: 'pointer',
			'padding-left': '1px',
			'padding-right': '1px'
		},
		title: watchType.remove,
		events: {
			click: function() {
				GM_deleteValue('selected' + watchType.gm_keyPostfix + '.' + watchType.id);
				if (watchType.discussPage === true || watchType.helpForum === true) {
					// removeThreadsFromCache
					var groupname = watchType.id;
					var reMatch = new RegExp("^" + groupname + "\\.");
					var threadKeys = GM_listKeys(reMatch); // do not use the callback version => delete creates havoc
					$each(threadKeys, function(key) {
						GM_log("DEBUG: removing key '" + key + "'");
						GM_deleteValue(key);
					});
				}
				this.set('id', 'toberemoved');

				addPlusIcon(watchType).replaces($(this));
				
				// the following code could error out if we are on an item that has a key set, but with an invalid object
				resetPostsIcon(watchType.gm_keyPostfix, watchType.id).dispose();
			}
		}
	}).inject($('NFPN.envelopes'));
	return icon;
}

function addPlusIcon(watchType) {
	if ($chk($('NFPN.plusmin.icon.' + watchType.gm_keyPostfix + '.' + watchType.id))) { // already there
		return $('NFPN.plusmin.icon.' + watchType.gm_keyPostfix + '.' + watchType.id);
	}

	var icon = new Element('img', {
		src: images.plus,
		id: 'NFPN.plusmin.icon.' + watchType.gm_keyPostfix + '.' + watchType.id,
		height: '10px',
		styles: {
			cursor: 'pointer',
			'padding-left': '1px',
			'padding-right': '1px'
		},
		title: watchType.add,
		events: {
			click: function() {
				try {
					createPostsIcon({
						prefix: watchType.gm_keyPostfix,
						id: watchType.id,
						url: "javascript:void(0);",
						title: watchType.title,
						isStored: false
					});
					var newPostsIcon = $('NFPN.' + watchType.gm_keyPostfix + '.' + watchType.id);
					newPostsIcon.src = images.ok; // for some reason, the ok image is not shown
					newPostsIcon.height = 16;
			
					var processPageFunction = undefined;
					if (watchType.discussPage === true) {
						var groupId = GM_getGroupId();
						if (watchType.fromGroupPage) {
							GM_storeObject('selectedGroup.' + watchType.id, {
								groupname: watchType.id,
								groupId: groupId,
								title: watchType.title
							});
						} else {
							processPageFunction = processDiscussPage;
							var groupTitle = readGroupTitleFromPage();
							GM_storeObject('selectedGroup.' + watchType.id, {
								groupname: watchType.id,
								title: groupTitle,
								groupId: groupId
							});
							watchType.title = groupTitle;
						}

					} else if (watchType.helpForum === true) {
						processPageFunction = processHelpForumPage;
						var forumTitle = $('Tertiary').textContent.replace(/\n[^]*$/im, '');
						GM_storeObject('selectedHelpforum.' + watchType.id, {
							groupname: watchType.id,
							title: forumTitle
						});
						watchType.title = forumTitle;

					} else if (watchType.discussThread === true) {
						processPageFunction = processThreadPage;
						var threadTitle = readThreadTitleFromPage();
						var titleHeader = $('Tertiary');
						var groupTitle = readGroupTitleFromPage();
						countPostsInThreadPage($(document), function (count) {
							if (!count.success) {
								notifyError(watchType.gm_keyPostfix, watchType.id, count.message);
								return;
							}
							var bgColor;
							var metaInfo = GM_getObject('selectedGroup.' + watchType.groupname);
							if (metaInfo.key_present) {
								var selectedGroup = metaInfo.value;
								if (selectedGroup) {
									bgColor = selectedGroup.bgColor;
								}
							}
							GM_storeObject('selectedThread.' + watchType.id, {
								topic: watchType.id,
								groupname: watchType.groupname,
								groupTitle: groupTitle,
								groupId: groupId,
								title: threadTitle,
								replies: count.items,
								bgColor: bgColor
							});
						});
						watchType.title = threadTitle;

					} else if (watchType.forumItem === true) {
						processPageFunction = processForumItemPage;
						var threadTitle = $('GoodStuff').getElement('h2').get('html');
						countPostsInThreadPage($(document), function(count) {
							if (!count.success) {
								notifyError(watchType.gm_keyPostfix, watchType.id, count.message);
								return;
							}
							var forumTitle = getMain().getElements('h1 a')[1].textContent.replace(/\n[^]*$/im, '');
							GM_storeObject('selectedForumitem.' + watchType.id, {
								topic: watchType.id,
								groupTitle: forumTitle,
								groupname: watchType.groupname,
								title: threadTitle,
								replies: count.items
							});
						});
						watchType.title = threadTitle;

					} else if (watchType.photoPage === true) {
						processPageFunction = processPhotoPage;
						var main = getMain();
						if ($chk(main)) {
							var photoTitle = main.getElement('h1').get('html');
						} else {
							// new photo page
							photoTitle = $('content').getElement('span.photo-title').get('text');
						}
						countCommentsInPhotoPage(function (count) {
							if (!count.success) {
								notifyError(watchType.gm_keyPostfix, watchType.id, count.message);
								return;
							}
							GM_storeObject('selectedPhoto.' + watchType.id, {
								photoId: watchType.id,
								username: watchType.username,
								title: photoTitle,
								comments: count.items
							});
						});
						watchType.title = photoTitle;

					} else if (watchType.photoStream === true) {
						processPageFunction = processPhotostreamPage;
						countPhotosInPhotostream(function(count) {
							if (!count.success) {
								notifyError(watchType.gm_keyPostfix, watchType.id, count.message);
								return;
							}
							GM_storeObject('selectedPhotostream.' + watchType.id, {
								username: watchType.username,
								userNsid: watchType.id,
								title: watchType.username, // temporary
								photos: count.items
							});
						});
						watchType.title = watchType.username;

					} else if (watchType.groupmembers === true) {
						if (watchType.fromGroupPage) {
							var count = getMain().getElement('span.LinksNew');
							if ($chk(count)) { // oldLayout
								count = count.getElement('a[href*=members]').get('text');
							} else {
								count = getMain().getElement('div.group-members h1').get('text');
							}
							GM_storeObject('selectedGroupmembers.' + watchType.id, {
								groupId: watchType.id,
								title: watchType.title,
								members: parseInt(count.replace(/[^\d]/g, ''), 10)
							});
						} else {
							processPageFunction = processGroupMembersPage;
							countMembersInGroup(function(count) {
								if (!count.success) {
									notifyError(watchType.gm_keyPostfix, watchType.id, count.message);
									return;
								}
								GM_storeObject('selectedGroupmembers.' + watchType.id, {
									groupId: watchType.id,
									title: watchType.title,
									members: count.items
								});
							});
						}

					} else if (watchType.grouppool === true) {
						var reGroupnameMatch = /.*flickr.com\/groups\/([^\/]*)\/?/;
						var groupname = reGroupnameMatch.exec(document.location.href)[1];
						if (watchType.fromGroupPage &&
							$chk($('span_see_all'))) { // old navigation bar
								var count = $('span_see_all').get('text');
								GM_storeObject('selectedGrouppool.' + watchType.id, {
									groupId: watchType.id,
									groupname: groupname,
									title: watchType.title,
									photos: parseInt(count.replace(/[^\d]/g, ''), 10)
								});
						} else { // new navigation bar => pool/ pages no longer exist => count photos on group page
							processPageFunction = watchType.fromGroupPage ? processGroupPage : processGroupPoolPage;
							countPhotosInGroupPage(function(count) {
								if (!count.success) {
									notifyError(watchType.gm_keyPostfix, watchType.id, count.message);
									return;
								}
								GM_storeObject('selectedGrouppool.' + watchType.id, {
									groupId: watchType.id,
									groupname: groupname,
									title: watchType.title,
									photos: count.items
								});
							});
						}

					} else if (watchType.reversedcontacts === true) {
						processPageFunction = processReversedContactsPage;
						countReversedContacts($(document), function (count) {
							if (!count.success) {
								notifyError(watchType.gm_keyPostfix, watchType.id, count.message);
								return;
							}
							var username = /www.flickr.com\/people\/([^\/]+)\/contacts\/rev/.exec(document.location.href)[1];
							GM_storeObject('selectedReversedcontacts.' + watchType.id, {
								userNsid: watchType.id,
								username: username,
								title: watchType.title,
								members: count.items
							});
						});

					} else if (watchType.pendingitems === true) {
						if (watchType.fromGroupPage) {
							GM_storeObject('selectedPendingitems.' + watchType.id, {
								groupname: watchType.id,
								title: watchType.title,
								items: 0
							});

						} else {
							processPageFunction = processPendingItemsPage;
							countPendingItems($(document), function(count) {
								if (!count.success) {
									notifyError(watchType.gm_keyPostfix, watchType.id, count.message);
									return;
								}
								GM_storeObject('selectedPendingitems.' + watchType.id, {
									groupname: watchType.id,
									title: watchType.title,
									items: count.items
								});
							});
						}

					} else if (watchType.app === true) {
						processPageFunction = processAppPage;
						countCommentsInAppPage($(document), function(count) {
							if (!count.success) {
								notifyError(watchType.gm_keyPostfix, watchType.id, count.message);
								return;
							}
							GM_storeObject('selectedApp.' + watchType.id, {
								appId: watchType.id,
								username: watchType.username,
								title: watchType.title,
								comments: count.items
							});
						});

					} else if (watchType.set === true) {
						processPageFunction = processSetPage;
						countCommentsInSetPage($(document), function(count) {
							if (!count.success) {
								notifyError(watchType.gm_keyPostfix, watchType.id, count.message);
								return;
							}
							GM_storeObject('selectedSet.' + watchType.id, {
								setId: watchType.id,
								username: watchType.username,
								realname: watchType.realname,
								title: watchType.title,
								comments: count.items
							});
						});

					} else if (watchType.pendingtestimonials === true) {
						processPageFunction = processPendingTestimonialsPage;
						countPendingTestimonials($(document), function(count) {
							if (!count.success) {
								notifyError(watchType.gm_keyPostfix, watchType.id, count.message);
								return;
							}
							GM_storeObject('selectedPendingtestimonials.' + watchType.id, {
								userNsid: watchType.id,
								title: watchType.title,
								items: count.items
							});
						});

					} else if (watchType.pendingmembers === true) {
						if (watchType.fromGroupPage) {
							GM_storeObject('selectedPendingmembers.' + watchType.id, {
								groupId: watchType.id,
								title: watchType.title,
								items: 0
							});

						} else {
							processPageFunction = processPendingMembersPage;
							countPendingMembers($(document), function(count) {
								if (!count.success) {
									notifyError(watchType.gm_keyPostfix, watchType.id, count.message);
									return;
								}
								GM_storeObject('selectedPendingmembers.' + watchType.id, {
									groupId: watchType.id,
									title: watchType.title,
									items: count.items
								});
							});
						}
					
					} else if (watchType.favorites === true) {
						processPageFunction = processFavoritesPage;
						var main = getMain();
						countFavoritesForUser(function(count) {
							if (!count.success) {
								notifyError(watchType.gm_keyPostfix, watchType.id, count.message);
								return;
							}
							GM_storeObject('selectedFavorites.' + watchType.id, {
								userNsid: watchType.id,
								userpath: watchType.userpath,
								title: watchType.title,
								favorites: count.items
							});
						});
					}

					// this.dispose();
					this.set('id', 'toberemoved');

					addMinusIcon(watchType).replaces($(this));

					if (processPageFunction) {
						try {
							processPageFunction();
						} catch(e) {
							notifyError(watchType.gm_keyPostfix, watchType.id, e);
						}
					}
				} catch(e) {
					this.set('src', images.error);
					this.set('title', this.get('alt') + ": " + e);
					this.removeEvent('click');
					var newPostsIcon = $('NFPN.' + watchType.gm_keyPostfix + '.' + watchType.id);
					if ($chk(newPostsIcon)) newPostsIcon.dispose();
					return;
				}
			}
		}
	}).inject($('NFPN.envelopes'));
	return icon;
}

function addPlusMinIconOnHelpforumPage(data) {
	var topic = data.topic;
	var groupname = data.groupname;
	var cell = data.cell;
	var watchType = watchTypes.forumitem;
	if ($chk($('NFPN.plusmin.icon.thread.' + topic))) { // already present
		return;
	}
	cell.setStyle('white-space','nowrap');

	var threadInfo = GM_getObject('selected' + watchType.gm_keyPostfix + '.' + topic);

	if ($chk(threadInfo) && threadInfo.key_present) {
		new Element('img', {
			src: images.minus,
			id: 'NFPN.plusmin.icon.thread.' + topic,
			height: '8px',
			styles: {
				cursor: 'pointer'
			},
			title: watchType.remove,
			events: {
				click: function(evt) {
					var target = $(evt.target);
					var topic = target.get('id').replace('NFPN.plusmin.icon.thread.', '');
					target.dispose();
					GM_deleteValue('selected' + watchType.gm_keyPostfix + '.' + topic);
					addPlusMinIconOnHelpforumPage(data);
					// the following code could error out if we are on an item that has a key set, but with an invalid object
					resetPostsIcon(watchType.gm_keyPostfix, topic).dispose();
				}
			}
		}).inject(cell, 'top');
	} else {
		new Element('img', {
			src: images.plus,
			id: 'NFPN.plusmin.icon.thread.' + topic,
			height: '8px',
			styles: {
				cursor: 'pointer'
			},
			title: watchType.add,
			events: {
				click: function() {
					var threadTitle = this.getParent('tr').getElements('td')[0].getElement('a').textContent;
					var replies = parseInt(this.getParent('tr').getElements('td')[2].textContent.match(/^(\d+)/)[1]);
					var groupTitle = $$('h1 a')[0].textContent;
					var groupname = document.location.href.match(/.*flickr.com\/help\/forum\/([^\/]*)/)[1];
					var bgColor;
					var metaInfo = GM_getObject('selectedHelpforum.' + groupname);
					if (metaInfo.key_present) {
						var selectedGroup = metaInfo.value;
						if (selectedGroup) {
							bgColor = selectedGroup.bgColor;
						}
					}

					GM_storeObject('selectedForumitem.' + topic, {
						topic: topic,
						groupname: groupname,
						groupTitle: groupTitle,
						groupId: GM_getGroupId(),
						title: threadTitle,
						replies: replies,
						bgColor: bgColor
					});
					watchType.title = threadTitle;

					this.dispose();
					createPostsIcon({
						prefix: watchType.gm_keyPostfix,
						id: topic,
						url: "javascript:void(0);",
						title: threadTitle,
						isStored: true
					});
					var newPostsIcon = $('NFPN.' + watchType.gm_keyPostfix + '.' + topic);
					newPostsIcon.src = images.ok;
					newPostsIcon.height = 16;
					addPlusMinIconOnHelpforumPage(data);
				}
			}
		}).inject(cell, 'top');
	}
}

function addPlusMinIconOnDiscussPage(data) {
	var topic = data.topic;
	var groupname = data.groupname;
	var cell = data.cell;
	var watchType = watchTypes.thread;
	if ($chk($('NFPN.plusmin.icon.thread.' + topic))) { // already present
		return;
	}
	var oldLayout = $chk(getMain().getElement('table.TopicListing'));
	if (oldLayout) cell.setStyle('white-space','nowrap');

	var threadInfo = GM_getObject('selected' + watchType.gm_keyPostfix + '.' + topic);

	if ($chk(threadInfo) && threadInfo.key_present) {
		new Element('img', {
			src: images.minus,
			id: 'NFPN.plusmin.icon.thread.' + topic,
			height: '8px',
			styles: {
				cursor: 'pointer'
			},
			title: watchType.remove,
			events: {
				click: function(evt) {
					var target = $(evt.target);
					var topic = target.get('id').replace('NFPN.plusmin.icon.thread.', '');
					target.dispose();
					GM_deleteValue('selected' + watchType.gm_keyPostfix + '.' + topic);
					addPlusMinIconOnDiscussPage(data);
					// the following code could error out if we are on an item that has a key set, but with an invalid object
					resetPostsIcon(watchType.gm_keyPostfix, topic).dispose();
				}
			}
		}).inject(cell, 'top');
	} else {
		new Element('img', {
			src: images.plus,
			id: 'NFPN.plusmin.icon.thread.' + topic,
			height: '8px',
			styles: {
				cursor: 'pointer'
			},
			title: watchType.add,
			events: {
				click: function() {
					var titleColumn = -1;
					try {
						var headers = this.getParent('table').getElement('tr').getElements('th,td');
						if (headers[0].get('html').match(/UCP-ng/)) {
							titleColumn = 1;
						} else {
							titleColumn = 0;
						}
						var threadTitle = this.getParent('tr').getElements('td')[titleColumn].getElement('a').textContent;
						var replies = parseInt(this.getParent('tr').getElements('td')[titleColumn + 2].textContent.match(/^(\d+)/)[1]);
					} catch (e) { // new group layout
						threadTitle = this.getParent('li').getElement('div h3 a').get('text').trim();
						replies = parseInt(this.getParent('li').getElement('span.reply-count').get('text').match(/(\d+)/)[1], 10);
					}
					var groupTitle = readGroupTitleFromPage();
					var groupname = document.location.href.match(/.*flickr.com\/groups\/([^\/]+)/)[1];
					var bgColor;
					var metaInfo = GM_getObject('selectedGroup.' + groupname);
					if (metaInfo.key_present) {
						var selectedGroup = metaInfo.value;
						if (selectedGroup) {
							bgColor = selectedGroup.bgColor;
						}
					}

					GM_storeObject('selectedThread.' + topic, {
						topic: topic,
						groupname: groupname,
						groupTitle: groupTitle,
						groupId: GM_getGroupId(),
						title: threadTitle,
						replies: replies,
						bgColor: bgColor
					});
					watchType.title = threadTitle;

					this.dispose();
					createPostsIcon({
						prefix: watchType.gm_keyPostfix,
						id: topic,
						url: "javascript:void(0);",
						title: threadTitle,
						isStored: true
					});
					var newPostsIcon = $('NFPN.' + watchType.gm_keyPostfix + '.' + topic);
					newPostsIcon.src = images.ok;
					newPostsIcon.height = 16;
					addPlusMinIconOnDiscussPage(data);
				}
			}
		}).inject(cell, 'top');
	}
}

function checkForNewPosts() {
	// insert the preferences icon first
	addPreferencesMenuItem();
	// loop the watched items
	var keys = GM_listKeys(); // we need them all
	$each(preferencesInitData, function(initData) {
		try {
			var debug = false; // initData.type === 'group';
			var needsSeparator = false;
			var reKey = new RegExp('^selected' + initData.gm_keyPostfix + '\\.');
			var now = new Date().getTime();
			// bug fix: we should not check on the type's lastChecked time, but on the item's lastChecked time
			GM_deleteValue(initData.gm_keyPostfix + ".lastChecked");
			keys.forEach(function(key) {
				if (key.match(reKey)) {
					var metaInfo = GM_getObject(key);
					if (metaInfo.key_present && $chk(metaInfo.value)) {
						var storedInfo = metaInfo.value;
						// there could be selectedSomething.undefined keys in there!
						//if (debug) GM_log("DEBUG: key=" + key.replace(reKey, ''));
						if (key.replace(reKey, '') != 'undefined' && $chk(storedInfo)) {
							var keyParts = key.match(/^selected([^\.]+)\.(.*)$/);
							var gm_keyPostfix = keyParts[1];
							var id = keyParts[2];
							var envelope = createPostsIcon({
								prefix: gm_keyPostfix,
								id: id, 
								url: initData.urlCreator(storedInfo).linkURL, 
								title: initData.gm_keyPostfix + ":" + storedInfo.title, 
								bgColor: storedInfo.bgColor,
								isStored: true
							});
							if (key.match(/selectedGroup\./) && !$chk(storedInfo.groupId)) {
								envelope.fireEvent('needs-conversion');
							} else {
								if (storedInfo[initData.error] == true || storedInfo[initData.error] == 'true')  {
									envelope.fireEvent('show-error', storedInfo.errorMsg);
	    								setTimeout(function() {
										envelope.fireEvent('run-check');
            								}, 500);
								} else {
									envelope.fireEvent('set-count', { newPosts: storedInfo[initData.newitems], returnedThreads: storedInfo.returnedThreads });
								}
								if (storedInfo.locked) {
									envelope.fireEvent('mark-locked');
								}
							}
							needsSeparator = true;
							return;
						}
					}
					var type = initData.type;
					var url;
					var html;
					var report = reportMissingObject({
						type: type,
						url: url,
						html: html,
						key: key
					});
					try {
						if (key.match(/selectedPhoto\./)) {
							var photoId = /selectedPhoto.(.*)/.exec(key)[1]; // NOT \d+ : could be 'undefined'
							if (photoId.match(/^\d+$/)) photoId = photoId.match(/^(\d+)$/)[1];
							report.fireEvent('update-link', { url: 'http://www.flickr.com/photo.gne?id=' + photoId });

						} else if (key.match(/selectedPhotostream\./)) {
							var userNsid = /selectedPhotostream\.(.*)/.exec(key)[1];
							resolveUsername(userNsid, function(user) {
								report.fireEvent('update-link', { url: 'http://www.flickr.com/photos/' + ($chk(user.displaypath) ? user.displaypath : userNsid) + '/' });
							});

						} else if (key.match(/selectedThread\./)) {
							threadId = /selectedThread\.(.*)/.exec(key)[1];
							// we need missing 'groupname' => use API
							if (metaInfo.key_present) {
								var threadInfo = metaInfo.value;
								url = "http://www.flickr.com/groups/" + threadInfo.groupname + "/discuss/" + threadInfo.topic + "/";
							} else {
								url = "http://www.flickr.com/"
							}
							report.fireEvent('update-link', { url: url });

						} else if (key.match(/selectedGroup\./)) {
							var groupname = /selectedGroup\.(.*)/.exec(key)[1];
							report.fireEvent('update-link', { url: 'http://www.flickr.com/groups/' + groupname + '/discuss/' });

						} else if (key.match(/selectedHelpforum\./)) {
							var groupname = /selectedHelpforum\.(.*)/.exec(key)[1];
							report.fireEvent('update-link', { url: 'http://www.flickr.com/help/forum/' + groupname + '/' });

						} else if (key.match(/selectedForumitem\./)) {
							var topicId = /selectedForumitem.(.*)/.exec(key)[1];
							// we don't have a group name, but all topics are available in all languages :)
							// default to 'en-us'
							report.fireEvent('update-link', { url: 'http://www.flickr.com/help/forum/en-us/' + topicId + '/' });

						} else if (key.match(/selectedGroupmembers\./)) {
							var groupid = /selectedGroupmembers\.(.*)/.exec(key)[1];
							report.fireEvent('update-link', { url: 'http://www.flickr.com/groups_members.gne?id=' + groupid });

						} else if (key.match(/selectedGrouppool\./)) {
							var groupname = /selectedGrouppool\.(.*)/.exec(key)[1];
							report.fireEvent('update-link', { url: 'http://www.flickr.com/groups/' + groupname + '/pool/' });

						} else if (key.match(/selectedReversedcontacts\./)) {
							var userNsid = /selectedReversedcontacts\.(.*)/.exec(key)[1];
							resolveUsername(userNsid, function(user) {
								report.fireEvent('update-link', { url: 'http://www.flickr.com/people/' + ($chk(user.displaypath) ? user.displaypath : userNsid) + '/contacts/rev/' });
							});

						} else if (key.match(/selectedPendingitems\./)) {
							var groupname = /selectedPendingitems\.(.*)/.exec(key)[1];
							report.fireEvent('update-link', { url: 'http://www.flickr.com/groups/' + groupname + '/admin/pending/' });

						} else if (key.match(/selectedApp\./)) {
							var appId = /selectedApp\.(.*)/.exec(key)[1];
							if (appId.match(/^\d+/)) {
								url = 'http://www.flickr.com/services/apps/' + appId + '/';
							}
							if (metaInfo.key_present) {
								var appInfo = metaInfo.value;
								if (url == undefined) {
									url = 'http://www.flickr.com/services/apps/' + appInfo.appId + '/'
								}
								html = initData.titleTemplate.text
									.replace('%1', appInfo.title)
									.replace('%2', appInfo.username);
							}
							if (url == undefined)
								url = 'http://www.flickr.com/services/apps/' + appId + '/';
							report.fireEvent('update-link', { url: url, html: html });

						} else if (key.match(/selectedSet\./)) {
							var setId = /selectedSet\.(.*)/.exec(key)[1];
							constructSetUrl(setId, function (retval) {
								if (retval.success) {
									report.fireEvent('update-link', { url: retval.url });
								}
							});

						} else if (key.match(/selectedPendingtestimonials\./)) {
							report.fireEvent('update-link', { url: 'http://www.flickr.com/testimonials_manage.gne' });

						} else if (key.match(/selectedPendingmembers\./)) {
							var groupid = /selectedPendingmembers\.(.*)/.exec(key)[1];
							report.fireEvent('update-link', { url: 'http://www.flickr.com/groups_pending.gne?id=' + groupid });
						
						} else if (key.match(/selectedFavorites\./)) {
							var userNsid = /selectedFavorites\.(.*)/.exec(key)[1];
							resolveUsername(userNsid,function (user) {
								report.fireEvent('update-link', { url: 'http://www.flickr.com/photos/' + ($chk(user.displaypath) ? user.displaypath : userNsid) + '/favorites/' });
							});

						}
					} catch(e) {
					}

				}
			});
			if (needsSeparator) {
				// separator
				new Element('span', {
					html: ' ',
					styles: {
						'border-right': '1px dotted #BABABA',
						margin: '0 1px 0 0px',
						padding: '0 0 0 0',
						width: 1
					}
				}).inject(preferences.refreshButtonAsLast() ? $('NFPN.reload.icon') : $('NFPN.envelopes'),
					  preferences.refreshButtonAsLast() ? 'before' : 'bottom');
				new Element('span', {
					html: ' ',
					styles: {
						margin: '0 0 0 1px',
						padding: '0 0 0 0',
						width: 1
					}
				}).inject(preferences.refreshButtonAsLast() ? $('NFPN.reload.icon') : $('NFPN.envelopes'),
					  preferences.refreshButtonAsLast() ? 'before' : 'bottom');
			}
		} catch(e) {
			GM_log("error in checkForNewPosts: " + e + " - " + initData.gm_keyPostfix);
		}
	});
}

function resetGroupEnvelopeCounts(groupname, gm_keyPostfix) {
	var metaInfo = GM_getObject('selected' + gm_keyPostfix + '.' + groupname);
	if (metaInfo.key_present // function should not be called otherwise
	&& ! $chk(metaInfo.value)) {
		return;
	}
	var selectedGroup = metaInfo.value;
	selectedGroup.invalidCount = 'true';
	// store as fast as possible
	GM_storeObject('selected' + gm_keyPostfix + '.' + groupname, selectedGroup);

	// update the envelope count for the group
	resetPostsIcon(gm_keyPostfix, groupname);
}

function markThreadAsRead(replyCell, groupname, threadInfo, topic, replies, title) {
	var gm_keyPostfix = preferencesInitData.group.gm_keyPostfix;
	var gm_keyPostfixThread = preferencesInitData.thread.gm_keyPostfix;
	// uncolorise
	replyCell.removeClass('NFPN_hint');
	replyCell.getElements('span.NFPN-some-new').dispose();

	// update thread info within this group
	if ($chk(threadInfo)) {
		threadInfo.replies = replies;
		threadInfo.newTopic = 'false';
		threadInfo.returnedThread = 'false';
		threadInfo.deletedItems = 'false';
		threadInfo.newPosts = 0;
		GM_storeObject(groupname + '.' + threadInfo.topic, threadInfo);
	} else {
		GM_storeObject(groupname + '.' + topic, {
			groupname: groupname,
			topic: topic,
			replies: replies,
			title: title
		});
	}

	resetGroupEnvelopeCounts(groupname, gm_keyPostfix);

	// reset the thread (envelope), if also watched
	var reTopicMatch = /.*flickr.com\/groups\/[^\/]*\/discuss\/(\d+)\//;
	var topicId = reTopicMatch.exec(topic)[1];
	var watchedThreadObject = GM_getObject('selected' + gm_keyPostfixThread + '.' + topicId);
	if (watchedThreadObject.key_present) {
		var watchedThread = watchedThreadObject.value;
		if (watchedThread) {
			watchedThread.replies = replies;
			watchedThread.title = title;
			watchedThread.newTopic = 'false';
			watchedThread.returnedThread = 'false';
			watchedThread.deletedItems = 'false';
			watchedThread.newPosts = 0;
			watchedThread.invalidCount = 'true'; // make sure to reread on next invocation
			GM_storeObject('selected' + gm_keyPostfixThread + '.' + topicId, watchedThread);
			resetPostsIcon(gm_keyPostfixThread, topicId);
		}
	}
}

function markForumitemAsRead(replyCell, groupname, threadInfo, topic, replies, title) {
	var gm_keyPostfix = preferencesInitData.helpforum.gm_keyPostfix;
	var gm_keyPostfixThread = preferencesInitData.forumitem.gm_keyPostfix;
	// uncolorise
	replyCell.removeClass('NFPN_hint');
	replyCell.getElements('span').dispose();

	// update thread info within this group
	if ($chk(threadInfo)) {
		threadInfo.replies = replies;
		threadInfo.newTopic = 'false';
		threadInfo.returnedThread = 'false';
		threadInfo.deletedItems = 'false';
		threadInfo.newPosts = 0;
		GM_storeObject(groupname + '.' + threadInfo.topic, threadInfo);
	} else {
		GM_storeObject(groupname + '.' + topic, {
			groupname: groupname,
			topic: topic,
			replies: replies,
			title: title
		});
	}

	resetGroupEnvelopeCounts(groupname, gm_keyPostfix);

	// reset the thread (envelope), if also watched
	var reTopicMatch = /.*flickr.com\/help\/forum\/[^\/]*\/(\d+)\//;
	var topicId = reTopicMatch.exec(topic)[1];
	var watchedThreadObject = GM_getObject('selected' + gm_keyPostfixThread + '.' + topicId);
	if (watchedThreadObject.key_present) {
		var watchedThread = watchedThreadObject.value;
		if (watchedThread) {
			watchedThread.replies = replies;
			watchedThread.title = title;
			watchedThread.newTopic = 'false';
			watchedThread.returnedThread = 'false';
			watchedThread.deletedItems = 'false';
			watchedThread.newPosts = 0;
			watchedThread.invalidCount = 'true'; // make sure to reread on next invocation
			GM_storeObject('selected' + gm_keyPostfixThread + '.' + topicId, watchedThread);
			resetPostsIcon(gm_keyPostfixThread, topicId);
		}
	}
}

function createMarkThreadAsReadEnvelope(data) {
	var element = data.element;
	var newPosts = data.newPosts;
	var groupname = data.groupname;
	var threadInfo = data.threadInfo;
	var topic = data.topic;
	var replies = data.replies;
	var title = data.title;
	var buttonClass = data.buttonClass;

	element.adopt(
	    new Element('span', {
		title: isNaN(newPosts) ? (newPosts === '?' ? 'posts deleted': 'some new') : newPosts + ' new',
		html: '(' + (isNaN(newPosts) ? newPosts: newPosts.toLocaleString()) + '&nbsp;new)',
		'class': 'NFPN-some-new'
	    }).adopt(
		new Element('img', {
			title: "Click to mark this thread as read",
			src: images.mailicon,
			'class': buttonClass,
			styles: {
				cursor: 'pointer',
				maxWidth: '12px'
			},
			events: {
				click: function(e) {
					markThreadAsRead(this.getParent('div.hd') || this.getParent('td'), groupname, threadInfo, topic, replies, title); // td: old group layout
				}
			}
		})
	    )
	);
	element.addClass('NFPN_hint');
	var oldLayout = $chk(getMain().getElement('table.TopicListing'));
	if (oldLayout) element.style.align = 'right';
}

function createMarkForumitemAsReadEnvelope(data) {
	var column = data.column;
	var newPosts = data.newPosts;
	var groupname = data.groupname;
	var threadInfo = data.threadInfo;
	var topic = data.topic;
	var replies = data.replies;
	var title = data.title;
	var buttonClass = data.buttonClass;

	column.adopt(
	    new Element('span', {
		title: isNaN(newPosts) ? (newPosts === '?' ? 'posts deleted': 'some new') : newPosts + ' new',
		html: '(' + (isNaN(newPosts) ? newPosts: newPosts.toLocaleString()) + '&nbsp;new)'
	    }).adopt(
		new Element('img', {
			title: "Click to mark this thread as read",
			src: images.mailicon,
			'class': buttonClass,
			styles: {
				cursor: 'pointer',
				maxWidth: 12
			},
			events: {
				click: function(e) {
					markForumitemAsRead(this.getParent('td'), groupname, threadInfo, topic, replies, title);
				}
			}
		})
	    )
	);
	column.addClass('NFPN_hint');
	column.style.align = 'right';
}

function processHelpForumPage() {
	try {
		var reGroupnameMatch = /.*flickr.com\/help\/forum\/([^\/]*)\//;
		var gm_keyPostfix = preferencesInitData.helpforum.gm_keyPostfix;
		var groupname = reGroupnameMatch.exec(document.location.href)[1];

		var metaInfo = GM_getObject('selected' + gm_keyPostfix + '.' + groupname);
		if (metaInfo.key_present) {
			var storedInfo = metaInfo.value;
			if ($chk(storedInfo)) {
				// sync number of unread posts on group's envelope
				storedInfo.newPosts = 0;
				var grouptitle = $('Tertiary').textContent.replace(/\n[^]*$/im, '');
				storedInfo.title = grouptitle;
				// read all rows
				// for every row, compare with the stored value
				// remove threads from cache that are no longer on the page
				var storedThreads = retrieveThreadsFromCache(groupname);
				var newlyAddedGroup = (storedThreads.getKeys().length === 0);
				try {
					getMain().getElements('table.TopicListing').each(function(table, tableIdx) { // helpforum has two tables
						table.getElements('tr').each(function(row, rowIdx) {
							var columns = row.getElements('th,td'); // Discuss page uses th, forum uses td
							if (rowIdx === 0) { // header
								// add 'mark all read' icon
								var repliesHeader = columns[2];
								repliesHeader.adopt(
								    new Element('img', {
									title: "Click to mark all threads as read",
									'class': 'NFPN-mark-all-button',
									src: images.mailicon,
									styles: {
										cursor: 'pointer',
										maxWidth: 12
									},
									events: {
										click: function(evt) {
											$$('img.NFPN-mark-as-read-button-' + tableIdx).each(function(button) {
												button.fireEvent('click');
											});
											resetGroupEnvelopeCounts(groupname, gm_keyPostfix);
										}
									}
								    })
								);
								return;
							} // end header
							//$$('span.New').dispose();
							var title = columns[0].getElement('a').textContent;
							var topic = columns[0].getElement('a').href.replace(/\/lastpage/g, '');
							var replies = parseInt(columns[2].textContent.replace(/(\.|,)/g, ''), 10);

							if (newlyAddedGroup) {
								GM_storeObject(groupname + '.' + topic, {
									groupname: groupname,
									topic: topic,
									replies: replies,
									title: title
								});
								return;
							}
							var threadInfo = storedThreads.get(topic);
							if ($chk(threadInfo)) {
								if (threadInfo.returnedThread === 'true' || threadInfo.deletedItems === 'true') {
									// new post(s): colorize
									storedInfo.newPosts += 1;
									storedInfo.returnedThreads = 'true';
									createMarkForumitemAsReadEnvelope({
										column: columns[2],
										newPosts: threadInfo.deletedItems === 'true' ? '?': 'x',
										groupname: groupname,
										threadInfo: threadInfo,
										topic: topic,
										replies: replies,
										title: title,
										buttonClass: 'NFPN-mark-as-read-button-' + tableIdx
									});

								} else if (threadInfo.replies < replies || threadInfo.newTopic === 'true') {
									// new posts: colorize
									var newPosts = (threadInfo.newTopic === 'true' ? replies + 1: replies - threadInfo.replies);
									threadInfo.newPosts = newPosts;
									storedInfo.newPosts += newPosts;
									GM_storeObject(groupname + '.' + threadInfo.topic, threadInfo);
									createMarkForumitemAsReadEnvelope({
										column: columns[2],
										newPosts: newPosts,
										groupname: groupname,
										threadInfo: threadInfo,
										topic: topic,
										replies: replies,
										title: title,
										buttonClass: 'NFPN-mark-as-read-button-' + tableIdx
									});

								} else if (threadInfo.replies > replies) { // someone deleted a comment; record
									// treat it as a returning thread
									storedInfo.returnedThreads = 'true';
									storedInfo.newPosts += 1;
									threadInfo.replies = replies;
									threadInfo.newPosts = 0;
									threadInfo.deletedItems = 'true';
									GM_storeObject(groupname + '.' + threadInfo.topic, threadInfo);
									createMarkForumitemAsReadEnvelope({
										column: columns[2],
										newPosts: '?',
										groupname: groupname,
										threadInfo: threadInfo,
										topic: topic,
										replies: replies,
										title: title,
										buttonClass: 'NFPN-mark-as-read-button-' + tableIdx
									});

								} else { // threadInfo.replies == replies => reset 'newPosts'
									threadInfo.newPosts = 0;
									GM_storeObject(groupname + '.' + threadInfo.topic, threadInfo);

								}

								storedThreads.erase(topic);

							} else {
								if (replies === 0) {
									// new topic: colorize
									createMarkForumitemAsReadEnvelope({
										column: columns[2],
										newPosts: 1,
										groupname: groupname,
										topic: topic,
										replies: 0,
										title: title,
										buttonClass: 'NFPN-mark-as-read-button-' + tableIdx
									});
									// and store
									GM_storeObject(groupname + '.' + topic, {
										groupname: groupname,
										topic: topic,
										replies: 0,
										newPosts: 1,
										title: title,
										newTopic: 'true'
									});
									storedInfo.newPosts += 1;
								} else {
									// could be a thread returning from pageX
									createMarkForumitemAsReadEnvelope({
										column: columns[2],
										newPosts: 'x',
										groupname: groupname,
										threadInfo: undefined,
										topic: topic,
										replies: replies,
										title: title,
										buttonClass: 'NFPN-mark-as-read-button-' + tableIdx
									});
									// in any case, don't store! => if not stored, this thread will not count as an extra 1+ on other pages!
									storedInfo.newPosts += 1;
									storedInfo.returnedThreads = 'true';
									GM_storeObject(groupname + '.' + topic, {
										groupname: groupname,
										topic: topic,
										replies: replies,
										newPosts: 0,
										title: title,
										returnedThread: 'true'
									});
								}
							}
						});
					});
					// those that are still in the hash, but have not been recognized, have fallen of the page
					storedThreads.getKeys().each(function(key) {
						GM_deleteValue(groupname + '.' + key);
					});
					// older versions didn't store the group id, needed in the request for RSS feed
					if ((!$chk(storedInfo.groupId) && ! $chk(storedInfo.privateGroup)) || ! $chk(storedInfo.title)) {
						var groupId = GM_getGroupId();
						var title = $('Tertiary').textContent.replace(/\n[^]*$/im, '');
						storedInfo.groupId = groupId;
						storedInfo.privateGroup = ! $chk(groupId);
						storedInfo.title = title;
					}
				} catch(e) {
					GM_log("error processing Discusspage: " + e);
				}
				GM_storeObject('selected' + gm_keyPostfix + '.' + groupname, storedInfo);
				notifyNewPosts(gm_keyPostfix, groupname, storedInfo.newPosts, storedInfo.returnedThreads);
			}
			watchTypes.helpforum.id = groupname;
			addMinusIcon(watchTypes.helpforum);
		} else {
			watchTypes.helpforum.id = groupname;
			addPlusIcon(watchTypes.helpforum);
		}
		getMain().getElements('table.TopicListing').each(function(table) { // helpforum has two tables
			table.getElements('tr').each(function(row, index) {
				if (index === 0) { // header
					return;
				}
				var columns = row.getElements('td');
				var title = columns[0].getElement('a').textContent;
				var topic = columns[0].getElement('a').href;
				var replies = parseInt(columns[2].textContent.replace(/(\.|,)/g, ''), 10);
				var topicId = topic.match(/.*flickr.com\/help\/forum\/[^\/]+\/(\d+)/)[1]; 
				// add +/- icon for each thread
				addPlusMinIconOnHelpforumPage({
					topic: topicId,
					groupname: groupname,
					cell: columns[2]
				});
				var gotoLastPageLink = preferences.gotoLastThreadPageLink();
				if (gotoLastPageLink) {
					try {
						if (!columns[0].getElement('a').href.match(/\/lastpage/)) {
							columns[0].getElement('a').href = topic + '/lastpage';
						}
					} catch (e) {
						GM_log("error updating links: " + e);
					}
				}
			});
		});
	} catch(e) {
		GM_log("unable to read forum name: " + e);
	}
}

function pimpDiscussThreadInList(data) {
	var row = data.row;
	var topic = data.topic;
	var replies = data.replies;
	var title = data.title;
	var threadInfo = data.threadInfo;
	var groupInfo = data.groupInfo;
	var groupname = groupInfo.groupname;

	if ($chk(threadInfo)) {
		if (threadInfo.returnedThread === 'true' || threadInfo.deletedItems === 'true') {
			// new post(s): colorize
			groupInfo.newPosts += 1;
			groupInfo.returnedThreads = 'true';
			createMarkThreadAsReadEnvelope({
				element: row.getElement('div.hd'),
				newPosts: threadInfo.deletedItems === 'true' ? '?': 'x',
				groupname: groupname,
				threadInfo: threadInfo,
				topic: topic,
				replies: replies,
				title: title,
				buttonClass: 'NFPN-mark-as-read-button'
			});

		} else if (threadInfo.replies < replies || threadInfo.newTopic === 'true') {
			// new posts: colorize
			var newPosts = (threadInfo.newTopic === 'true' ? replies + 1: replies - threadInfo.replies);
			threadInfo.newPosts = newPosts;
			groupInfo.newPosts += newPosts;
			GM_storeObject(groupname + '.' + threadInfo.topic, threadInfo);
			createMarkThreadAsReadEnvelope({
				element: row.getElement('div.hd'),
				newPosts: newPosts,
				groupname: groupname,
				threadInfo: threadInfo,
				topic: topic,
				replies: replies,
				title: title,
				buttonClass: 'NFPN-mark-as-read-button'
			});

		} else if (threadInfo.replies > replies) { // someone deleted a comment; record
			// treat it as a returning thread
			groupInfo.returnedThreads = 'true';
			groupInfo.newPosts += 1;
			threadInfo.replies = replies;
			threadInfo.newPosts = 0;
			threadInfo.deletedItems = 'true';
			GM_storeObject(groupname + '.' + threadInfo.topic, threadInfo);
			createMarkThreadAsReadEnvelope({
				element: row.getElement('div.hd'),
				newPosts: '?',
				groupname: groupname,
				threadInfo: threadInfo,
				topic: topic,
				replies: replies,
				title: title,
				buttonClass: 'NFPN-mark-as-read-button'
			});

		} else { // threadInfo.replies == replies => reset 'newPosts'
			threadInfo.newPosts = 0;
			GM_storeObject(groupname + '.' + threadInfo.topic, threadInfo);

		}


	} else {
		if (replies === 0) {
			// new topic: colorize
			createMarkThreadAsReadEnvelope({
				element: row.getElement('div.hd'),
				newPosts: 1,
				groupname: groupname,
				topic: topic,
				replies: 0,
				title: title,
				buttonClass: 'NFPN-mark-as-read-button'
			});
			// and store
			GM_storeObject(groupname + '.' + topic, {
				groupname: groupname,
				topic: topic,
				replies: 0,
				newPosts: 1,
				title: title,
				newTopic: 'true'
			});
			groupInfo.newPosts += 1;
		} else {
			// could be a thread returning from pageX
			createMarkThreadAsReadEnvelope({
				element: row.getElement('div.hd'),
				newPosts: 'x',
				groupname: groupname,
				threadInfo: undefined,
				topic: topic,
				replies: replies,
				title: title,
				buttonClass: 'NFPN-mark-as-read-button'
			});
			// in any case, don't store! => if not stored, this thread will not count as an extra 1+ on other pages!
			groupInfo.newPosts += 1;
			groupInfo.returnedThreads = 'true';
			GM_storeObject(groupname + '.' + topic, {
				groupname: groupname,
				topic: topic,
				replies: replies,
				newPosts: 0,
				title: title,
				returnedThread: 'true'
			});
		}
	}
}

function processDiscussPage() {
	try {
		var reGroupnameMatch = /.*flickr.com\/groups\/([^\/]*)\//;
		var gm_keyPostfix = preferencesInitData.group.gm_keyPostfix;

		var groupId = GM_getGroupId();
		if (!$chk(groupId)) {
			GM_log("ERROR: unable to convert group!!");
			return;
		}
		var groupname = reGroupnameMatch.exec(document.location.href)[1];
		var groupInfo = GM_getObject('selectedGroup.' + groupname);
		if (groupInfo.key_present && !$chk(groupInfo.value.groupId)) {
			groupInfo.value.groupId = groupId;
			GM_storeObject('selectedGroup.' + groupname, groupInfo.value);
		}
		var oldLayout = $chk(getMain().getElement('table.TopicListing'));
		if (oldLayout) {
			var titleColumn = getMain().getElement('table.TopicListing').getElement('tr').getElement('th,td').get('html').match(/UCP-ng/) ? 1: 0;
		}
		var metaInfo = GM_getObject('selected' + gm_keyPostfix + '.' + groupname);
		if (metaInfo.key_present) {
			var storedInfo = metaInfo.value;
			if ($chk(storedInfo)) {
				// sync number of unread posts on group's envelope
				storedInfo.newPosts = 0;
				var grouptitle = readGroupTitleFromPage();
				storedInfo.title = grouptitle;
				// read all rows
				// for every row, compare with the stored value
				// remove threads from cache that are no longer on the page
				var storedThreads = retrieveThreadsFromCache(groupname);
				var newlyAddedGroup = (storedThreads.getKeys().length === 0);
				try {
					var markAllButton = new Element('img', {
							title: "Click to mark all threads as read",
							'class': 'NFPN-mark-all-button',
							src: images.mailicon,
							styles: {
								cursor: 'pointer',
								maxWidth: '12px'
							},
							events: {
								click: function(evt) {
									$$('img.NFPN-mark-as-read-button').each(function(button) {
										button.fireEvent('click');
									});
									resetGroupEnvelopeCounts(groupname, gm_keyPostfix);
								}
							}
					});
					if (oldLayout) {
						var header = getMain().getElement('table.TopicListing').getElement('tr');
						// add 'mark all read' icon
						var repliesHeader = header.getElements('th')[titleColumn + 2];
						repliesHeader.adopt(markAllButton);
					} else {
						// add 'mark all read' icon
						$$('div.groups-header h2')[0].adopt(markAllButton);
						markAllButton.setStyles('{ margin-left: 10px; }');
					}
					var rows = oldLayout ? getMain().getElement('table.TopicListing').getElements('tr') :
								$$('ul.topic-list li');
							
					rows.each(function(row, rowIdx) {
						if (oldLayout) {
							if (rowIdx === 0) { // header
								return;
							} // end header
							var columns = row.getElements('td');
							var title = columns[titleColumn].getElement('a').textContent;
							var topic = columns[titleColumn].getElement('a').href.replace(/\/lastpage/g, '');
							var replies = parseInt(columns[titleColumn + 2].textContent.replace(/(\.|,)/g, ''), 10);
						} else {
							var title = row.getElement('a').get('text');
							var topic = row.getElement('a').get('href').replace(/\/lastpage.*/g, '').replace(/\/page\d+.*/g, '');
							if (!topic.match(/^http/)) topic = "http://www.flickr.com" + topic;
							if (!topic.match(/\/$/))   topic = topic + '/';
							var replies = parseInt(row.getElement('span.reply-count').get('text').replace(/(\.|,)/g, ''), 10);
						}

						if (newlyAddedGroup) {
							GM_storeObject(groupname + '.' + topic, {
								groupname: groupname,
								topic: topic,
								replies: replies,
								title: title
							});
							return;
						}
						var threadInfo = storedThreads.get(topic);
						if (!oldLayout) {
							pimpDiscussThreadInList({ row: row, topic: topic, replies: replies, title: title, threadInfo: threadInfo, groupInfo: storedInfo });
							storedThreads.erase(topic);
							return;
						}
						// old layout only:
						if ($chk(threadInfo)) {
							if (threadInfo.returnedThread === 'true' || threadInfo.deletedItems === 'true') {
								// new post(s): colorize
								storedInfo.newPosts += 1;
								storedInfo.returnedThreads = 'true';
								createMarkThreadAsReadEnvelope({
									element: oldLayout ? columns[titleColumn + 2] : row.getElement('div.hd'),
									newPosts: threadInfo.deletedItems === 'true' ? '?': 'x',
									groupname: groupname,
									threadInfo: threadInfo,
									topic: topic,
									replies: replies,
									title: title,
									buttonClass: 'NFPN-mark-as-read-button'
								});

							} else if (threadInfo.replies < replies || threadInfo.newTopic === 'true') {
								// new posts: colorize
								var newPosts = (threadInfo.newTopic === 'true' ? replies + 1: replies - threadInfo.replies);
								threadInfo.newPosts = newPosts;
								storedInfo.newPosts += newPosts;
								GM_storeObject(groupname + '.' + threadInfo.topic, threadInfo);
								createMarkThreadAsReadEnvelope({
									element: oldLayout ? columns[titleColumn + 2] : row.getElement('div.hd'),
									newPosts: newPosts,
									groupname: groupname,
									threadInfo: threadInfo,
									topic: topic,
									replies: replies,
									title: title,
									buttonClass: 'NFPN-mark-as-read-button'
								});

							} else if (threadInfo.replies > replies) { // someone deleted a comment; record
								// treat it as a returning thread
								storedInfo.returnedThreads = 'true';
								storedInfo.newPosts += 1;
								threadInfo.replies = replies;
								threadInfo.newPosts = 0;
								threadInfo.deletedItems = 'true';
								GM_storeObject(groupname + '.' + threadInfo.topic, threadInfo);
								createMarkThreadAsReadEnvelope({
									element: oldLayout ? columns[titleColumn + 2] : row.getElement('div.hd'),
									newPosts: '?',
									groupname: groupname,
									threadInfo: threadInfo,
									topic: topic,
									replies: replies,
									title: title,
									buttonClass: 'NFPN-mark-as-read-button'
								});

							} else { // threadInfo.replies == replies => reset 'newPosts'
								threadInfo.newPosts = 0;
								GM_storeObject(groupname + '.' + threadInfo.topic, threadInfo);

							}

							storedThreads.erase(topic);

						} else {
							if (replies === 0) {
								// new topic: colorize
								createMarkThreadAsReadEnvelope({
									element: oldLayout ? columns[titleColumn + 2] : row.getElement('div.hd'),
									newPosts: 1,
									groupname: groupname,
									topic: topic,
									replies: 0,
									title: title,
									buttonClass: 'NFPN-mark-as-read-button'
								});
								// and store
								GM_storeObject(groupname + '.' + topic, {
									groupname: groupname,
									topic: topic,
									replies: 0,
									newPosts: 1,
									title: title,
									newTopic: 'true'
								});
								storedInfo.newPosts += 1;
							} else {
								// could be a thread returning from pageX
								createMarkThreadAsReadEnvelope({
									element: oldLayout ? columns[titleColumn + 2] : row.getElement('div.hd'),
									newPosts: 'x',
									groupname: groupname,
									threadInfo: undefined,
									topic: topic,
									replies: replies,
									title: title,
									buttonClass: 'NFPN-mark-as-read-button'
								});
								// in any case, don't store! => if not stored, this thread will not count as an extra 1+ on other pages!
								storedInfo.newPosts += 1;
								storedInfo.returnedThreads = 'true';
								GM_storeObject(groupname + '.' + topic, {
									groupname: groupname,
									topic: topic,
									replies: replies,
									newPosts: 0,
									title: title,
									returnedThread: 'true'
								});
							}
						}
					});
					// those that are still in the hash, but have not been recognized, have fallen of the page
					storedThreads.getKeys().each(function(key) {
						GM_deleteValue(groupname + '.' + key);
					});
					// older versions didn't store the group id, needed in the request for RSS feed
					if ((!$chk(storedInfo.groupId) && ! $chk(storedInfo.privateGroup)) || ! $chk(storedInfo.title)) {
						var groupId = GM_getGroupId();
						var title = readGroupTitleFromPage();
						storedInfo.groupId = groupId;
						storedInfo.privateGroup = ! $chk(groupId);
						storedInfo.title = title;
					}
				} catch(e) {
					GM_log("error processing Discusspage: " + e);
				}
				GM_storeObject('selected' + gm_keyPostfix + '.' + groupname, storedInfo);
				notifyNewPosts(gm_keyPostfix, groupname, storedInfo.newPosts, storedInfo.returnedThreads);
			}
			watchTypes.group.id = groupname;
			addMinusIcon(watchTypes.group);
		} else {
			watchTypes.group.id = groupname;
			addPlusIcon(watchTypes.group);
		}
		var rows = oldLayout ? 
			getMain().getElement('table.TopicListing').getElements('tr') : 
			$$('ul.topic-list li');
		rows.each(function(row, index) {
			if (oldLayout) {
				if (index === 0) { // header
					return;
				}
				var columns = row.getElements('td');
				var title = columns[titleColumn].getElement('a').textContent;
				var topic = columns[titleColumn].getElement('a').href;
				var replies = parseInt(columns[titleColumn + 2].textContent.replace(/(\.|,)/g, ''), 10);
			} else {
				var title = row.getElement('a').get('text');
				var topic = row.getElement('a').get('href');
				var replies = parseInt(row.getElement('span.reply-count').get('text').replace(/(\.|,)/g, ''), 10);
			}

			var topicId = topic.match(/.*\/groups\/[^\/]+\/discuss\/(\d+)/)[1];
			// add +/- icon for each thread
			// TODO: add to pimpDiscussThread
			addPlusMinIconOnDiscussPage({
				topic: topicId,
				groupname: groupname,
				cell: oldLayout ? columns[titleColumn + 2] : row.getElement('div h3')
			});
			var gotoLastPageLink = preferences.gotoLastThreadPageLink();
			if (gotoLastPageLink && oldLayout) {
				try {
				    if (!columns[titleColumn].getElement('a').href.match(/\/lastpage/)) {
					columns[titleColumn].getElement('a').href = topic + '/lastpage';
				    }
				} catch (e) {
					GM_log("error updating links: " + e);
				}
			}
		});
	} catch(e) {
		GM_log("unable to read groupname: " + e);
	}
}

function countPostsInThreadPage(element, callback) {
	var replies = 0;
	var paginator = element.getElement('div.Paginator');
	if (!$chk(paginator) || paginator.length === 0) {
		// count the replies
		if (element.getElements('td.Said').length == 0) {
			callback({ success: false, message: "no posts found" });
		}
		replies = element.getElements('td.Said').length - 1; // not the challenge announcement
	} else {
		var results = element.getElement('div.Results');
		replies = results.get('html').match(/\([^\d]*\d+[^\d]+\d+[^\d]+([\d\.,]+)/)[1];
		//    (2001 to 2045 of 2,500
		replies = replies.replace(/(\.|,)/g, '');
		replies = parseInt(replies, 10);
	}
	callback({ success: true, items: replies });
}

function lastPageInDiscussionThread(helpforum) {
	if (document.location.href.test(/lastpage/)) {
		return true;
	}
	// if there is only 1 page, it's the last :-)
	var paginators = $$('div.Paginator');
	if (paginators.length === 0) {
		return true;
	}
	var paginator = paginators[0];
	if ($chk(paginator.getElement('span.AtEnd'))) {
		return true;
	}
	return false;
}

function scrollToPost(oldestUnreadPost) {
    try {
	var position = oldestUnreadPost.getOffsets();
	var navbarOffset = 0;
	try {
		navbarOffset = parseInt($('global-nav').getComputedStyle('height'), 10);
	} catch (e) {
		GM_log("unable to compute nav-bar's height");
		navbarOffset = 0;
	}
	var envelopesOffset = 0;
	try {
		envelopesOffset = parseInt($('NFPN.envelopes').getComputedStyle('height'), 10);
	} catch (e) {
		GM_log("unable to compute envelopes' height");
		envelopesOffset = 0;
	}
	if (position.y > 10 || (navbarOffset > 0 && position.y > navbarOffset)) {
		$(window).scrollTo(0, position.y - 10 - navbarOffset - envelopesOffset);
	}
    } catch(e) {
	GM_log("getTop() error: " + e);
    }
}

function colorizeNewPostsInThreadPage(newPosts, helpforum) {
	if (newPosts && newPosts > 0) {
		// colorize the new posts (if on the last page)
		if ((preferences.colorizeNewPosts() || preferences.scrollToNewPosts()) && lastPageInDiscussionThread(helpforum)) {
			var replyElements = $('DiscussTopic').getElement('table.TopicReply').getElements('td.Who');
			var oldestUnreadPost;
			// colorize the unread ones
			replyElements.each(function(replyElement, idx) {
				if (idx < replyElements.length - newPosts) {
					return;
				}
				oldestUnreadPost = oldestUnreadPost || replyElement;
				if (preferences.colorizeNewPosts()) replyElement.addClass('NFPN_hint');
			});
			if (replyElements.length === newPosts - 1 && $$('div.Paginator').length === 0) { // also color the topic entry
				oldestUnreadPost = undefined; // reset
				if (preferences.colorizeNewPosts()) $('DiscussTopic').getElement('td.Who').addClass('NFPN_hint');
			}
			if (replyElements.length < newPosts && $$('div.Paginator').length > 0) { // also color the 'prev' button
				oldestUnreadPost = undefined; // reset
				if (preferences.colorizeNewPosts()) $$('a.Prev').setStyle('background-color', '#FFE28A'); // addClass('NFPN_hint') does not work, nor does setStyles({ .. })
			}
			if ($chk(oldestUnreadPost) && preferences.scrollToNewPosts()) {
				scrollToPost(oldestUnreadPost.getParent('tr'));
			}
		}
	}
}

function readThreadTitleFromPage(element) {
	var title = "No thread title found";
	if (!$chk(element)) {
		if ($chk($('GoodStuff'))) {
			title = $('GoodStuff').getElement('h2').get('html');
		} else if ($$('div.group-topic-detail-col h2').length > 0) {
			title = $$('div.group-topic-detail-col h2')[0].get('text').trim();
		}
	} else {
		if ($chk(element.getElement('td[id=GoodStuff]'))) {
			title = element.getElement('td[id=GoodStuff]').getElement('h2').get('html');
		} else if ($chk(element.getElement('div.group-topic-detail-col h2'))) {
			title = element.getElement('div.group-topic-detail-col h2').get('text').trim();
		}
	}
	return title;
}

function processThreadPage(helpforum) {
	if (document.location.href.match(/edit(?:\/)?$/)) {
		return;
	}
	try {
		var reGroupnameMatch = /.*flickr.com\/groups\/([^\/]+)\//;
		var reTopicMatch = /.*flickr.com\/groups\/[^\/]*\/discuss\/(\d+)/;
		var gm_keyPostfix = preferencesInitData.group.gm_keyPostfix;
		var gm_keyPostfixThread = preferencesInitData.thread.gm_keyPostfix;
		if (helpforum) {
			reGroupnameMatch = /.*flickr.com\/help\/forum\/([^\/]*)/;
			reTopicMatch = /.*flickr.com\/help\/forum\/[^\/]*\/(\d+)/;
			gm_keyPostfix = preferencesInitData.helpforum.gm_keyPostfix;
			gm_keyPostfixThread = preferencesInitData.forumitem.gm_keyPostfix;
		}
		var groupname = reGroupnameMatch.exec(document.location.href)[1];
		var topic = reTopicMatch.exec(document.location.href)[1];
		var threadMetaInfo = GM_getObject('selected' + gm_keyPostfixThread + '.' + topic);
		var groupMetaInfo = GM_getObject('selected' + gm_keyPostfix + '.' + groupname);

		if ((threadMetaInfo.key_present || groupMetaInfo.key_present) && lastPageInDiscussionThread(helpforum)) {
			var title = readThreadTitleFromPage();
			// thread, or group, is watched - update replies
			countPostsInThreadPage($(document), function(count) {
				if (!count.success) {
					notifyError(gm_keyPostfix, topic, count.message);
					return;
				}
				var newPostsOnThreadWatch = 0;
				// thread is watched
				if (threadMetaInfo.key_present) {
					var storedThreadInfo = threadMetaInfo.value;
					if ($chk(storedThreadInfo)) {
						// backporting data from older versions
						if (!$chk(storedThreadInfo.groupId)) {
							storedThreadInfo.groupId = GM_getGroupId();
						}
						newPostsOnThreadWatch = count.items - storedThreadInfo.replies;
						storedThreadInfo.replies = count.items;
						storedThreadInfo.newPosts = 0;
						storedThreadInfo.title = title;

						GM_storeObject('selected' + gm_keyPostfixThread + '.' + topic, storedThreadInfo);
						resetPostsIcon(gm_keyPostfixThread, storedThreadInfo.topic);
					} else {
						GM_log("ERROR: found a key without value!?");
					}
				}
				var newPostsOnGroupWatch = 0;
				// group is watched
				if (groupMetaInfo.key_present) {
					var storedGroupInfo = groupMetaInfo.value;
					if ($chk(storedGroupInfo)) {
						if (!$chk(storedGroupInfo.groupId)) {
							storedGroupInfo.groupId = GM_getGroupId();
							GM_storeObject('selected' + gm_keyPostfix + '.' + groupname, storedGroupInfo);
						}
						var url = helpforum ?
							document.location.href.match(/(.*flickr.com\/help\/forum\/[^\/]+\/\d+\/)/)[1] : // not the 'page3' part, or '#reply', or ...
							document.location.href.match(/(.*flickr.com\/groups\/[^\/]+\/discuss\/\d+(?:\/)?)/)[1]; // not the 'page3' part, or '#reply', or ...
						if (!url.match(/\/$/)) url = url + '/';
						var metaInfo = GM_getObject(groupname + '.' + url); // constructing this url could be errorprone
						// we found thread info within the group's data
						if (metaInfo.key_present) {
							var threadInfo = metaInfo.value;
							if ($chk(threadInfo)) {
								newPostsOnGroupWatch = count.items - threadInfo.replies + (threadInfo.newTopic == 'true' ? 1 : 0);
								threadInfo.returnedThread = 'false';
								threadInfo.deletedItems = 'false';
								threadInfo.newPosts = 0;
								threadInfo.replies = count.items;
								threadInfo.newTopic = 'false';
								GM_storeObject(groupname + '.' + url, threadInfo);
							}
						} else {
							GM_storeObject(groupname + '.' + url, {
								groupname: groupname,
								topic: url,
								replies: count.items,
								title: title,
								newTopic: count.items == 0 ? 'true' : 'false'
							});
						}
								
						$('NFPN.' + gm_keyPostfix + '.' + groupname).fireEvent('run-check');
					}
				}
				if (newPostsOnGroupWatch > 0 || newPostsOnThreadWatch > 0) {
					colorizeNewPostsInThreadPage(Math.max(newPostsOnGroupWatch, newPostsOnThreadWatch), helpforum);
				}
			});
		} // last page
		if (threadMetaInfo.key_present) {
			if (helpforum) {
				watchTypes.forumitem.id = topic;
				watchTypes.forumitem.groupname = groupname;
				addMinusIcon(watchTypes.forumitem);
			} else {
				watchTypes.thread.id = topic;
				watchTypes.thread.groupname = groupname;
				addMinusIcon(watchTypes.thread);
			}
		} else {
			if (helpforum) {
				watchTypes.forumitem.id = topic;
				watchTypes.forumitem.groupname = groupname;
				addPlusIcon(watchTypes.forumitem);
			} else {
				watchTypes.thread.id = topic;
				watchTypes.thread.groupname = groupname;
				addPlusIcon(watchTypes.thread);
			}
		}

		// group is watched
		if (groupMetaInfo.key_present) {
			if ($$('div.group-topics ul.topic-list li.topic div.hd').length > 0) {
				$$('div.group-topics ul.topic-list li.topic').each(function(row) {
					var title = row.getElement('a').get('text');
					var topic = row.getElement('a').get('href').replace(/\/lastpage.*/g, '').replace(/\/page\d+.*/g, '');
					if (!topic.match(/^http/)) topic = "http://www.flickr.com" + topic;
					if (!topic.match(/\/$/))   topic = topic + '/';
					var replies = parseInt(row.getElement('span.reply-count').get('text').replace(/(\.|,)/g, ''), 10);
					var threadInfo = GM_getObject(groupname + '.' + topic);
					pimpDiscussThreadInList({ row: row, topic: topic, replies: replies, title: title, threadInfo: threadInfo.key_present ? threadInfo.value : null, groupInfo: groupMetaInfo.value });
				});
				return;
			}
			// show number of new posts in the left column - oldLayout, and help forum
			$$('table.TopicListing tr td').each(function(threadCell) {
				if ($chk(threadCell.getElement('span.NFPN_hint'))) return; // only show it once
				var topicUrlAnchor = threadCell.getElement('a');
				if (!topicUrlAnchor) {
					return;
				}
				var topicUrlMatch = topicUrlAnchor.href.match(/.*\/groups\/[^\/]+\/discuss\/\d+\//); // drop pageX
				if (!topicUrlMatch && helpforum) {
					topicUrlMatch = topicUrlAnchor.href.match(/.*\/help\/forum\/[^\/]+\/\d+\//);
				}
				if (!topicUrlMatch) {
					return;
				}
				var topicUrl = topicUrlMatch[0];
				if (!topicUrl.match(/^http/)) topicUrl = "http://www.flickr.com" + topicUrl;
				var metaInfo = GM_getObject(groupname + '.' + topicUrl);
				if (metaInfo.key_present) {
					var threadInfo = metaInfo.value;
					if (threadInfo) {
						var insertionPoint = helpforum ? topicUrlAnchor : threadCell;
						if (threadInfo.returnedThread === 'true') {
							new Element('span', {
								'class': 'NFPN_hint',
								html: ' (x&nbsp;new)'
							}).inject(insertionPoint);
							insertionPoint.addClass('NFPN_hint');
						} else if (threadInfo.deletedItems === 'true') {
							new Element('span', {
								'class': 'NFPN_hint',
								html: ' (?&nbsp;new)'
							}).inject(insertionPoint);
							insertionPoint.addClass('NFPN_hint');
						} else if (threadInfo.newTopic === 'true') {
							new Element('span', {
								'class': 'NFPN_hint',
								html: ' (' + (threadInfo.replies + 1).toLocaleString() + '&nbsp;new)'
							}).inject(insertionPoint);
							insertionPoint.addClass('NFPN_hint');
						} else if (threadInfo.newPosts && threadInfo.newPosts > 0) {
							new Element('span', {
								'class': 'NFPN_hint',
								html: ' (' + threadInfo.newPosts.toLocaleString() + '&nbsp;new)'
							}).inject(insertionPoint);
							insertionPoint.addClass('NFPN_hint');
						}
					}
				} else {
					topicUrlAnchor.adopt(new Element('span', {
						'class': 'NFPN_hint',
						html: ' (x&nbsp;new)'
					}));
					topicUrlAnchor.addClass('NFPN_hint');
				}
			});
		}
	} catch(e) { // hiccups ??
		GM_log("error processing thread page: " + e);
	}
}

function processForumItemPage() {
	processThreadPage(true);
}

function lastPageInPhotoPage() {
	var lastpage = false;
	// if there is only 1 page, it's the last :-)
	var paginator = $$('div.Paginator');
	if (paginator.length === 0) {
		lastpage = true;
	} else {
		if ($chk(paginator.getElement('span.AtEnd'))) {
			lastpage = true;
		}
	}
	return lastpage;
}

function colorizeNewPostsInPhotoPage(newPosts) {
	// no longer applicable on photo pages
}

function countCommentsInPhotoPage(callback) {
	var comments = 0;
	var commentsButton = $('button-bar-comment');
	if ($chk(commentsButton)) {
		comments = parseInt(commentsButton.get('text', 10));
		callback({ success: true, items: comments });
		return;
	}
	// new photo page layout: when called, there is no comments button yet
	// => use the API
	var photoId = document.location.href.match(/.*flickr.com\/photos\/[^\/]+\/(\d+)/)[1];
	apiData = {
		api_key: GM_getMagisterLudi(),
		auth_hash: GM_getAuthHash(),
		csrf: GM_getCsrf(),
		auth_token: GM_getAuthToken(),
		format: 'json',
		method: 'flickr.photos.getInfo',
		nojsoncallback: 1,
		photo_id: photoId
	};
	new Request({
		url: 'http://www.flickr.com',
		onSuccess: function(responseText, responseXML) {
			var result = JSON.parse(responseText);
			if (result.stat === 'fail') {
				callback({ success: false, message: "error reading photo information: " + result.message });
				return;
			}
			callback({ success: true, items: $chk(result.photo) && $chk(result.photo.comments) ? result.photo.comments._content : 0 });
		},
		onFailure: function(response) {
			callback({ success: false, message: "failure reading photo information: " + response.statusText});
		}
	}).get('/services/rest', apiData);
}

function processSomethingPage(data) {
	var debug = data.debug;
	var initData = preferencesInitData[data.type];
	var watchType = watchTypes[data.type];
	var id = data.id;
	var colorizeFunction = $chk(data.colorizeFunction) ? data.colorizeFunction: function() {};
	var countItemsFunction = $chk(data.countItemsFunction) ? data.countItemsFunction: function(callback) { callback({ success: true, items: 0 }); };
	var lastPageFunction = $chk(data.lastPageFunction) ? data.lastPageFunction: function() { return true; };
	var updateInfo = $chk(data.updateInfo) ? data.updateInfo: function() {};
	var updateIcon = $chk(data.updateIcon) ? data.updateIcon: function() {};

	if (debug) GM_log("DEBUG: reading 'selected" + initData.gm_keyPostfix + '.' + id + "'");
	var metaInfo = GM_getObject('selected' + initData.gm_keyPostfix + '.' + id);
	if (metaInfo.key_present) {
		var storedInfo = metaInfo.value;
		if ($chk(storedInfo)) {
			if (debug) GM_log("DEBUG: using stored info");
			if (lastPageFunction()) { // reset numbers, only when reading the last page of the thread
				if (debug) GM_log("DEBUG: on last page");
				countItemsFunction(function (count) {
					if (!count.success) {
						notifyError(initData.gm_keyPostfix, id, count.message);
						return;
					}
					colorizeFunction(Math.max(storedInfo[initData.newitems], count.items - storedInfo[initData.items]));
					if (debug) GM_log(["DEBUG: storing", 
						'selected' + initData.gm_keyPostfix + '.' + id,
						initData.items + ':' + count.items,
						initData.newitems + ':' + 0].join('\n'));
					if (count.pending) {
						storedInfo[initData.items] = 0;
						storedInfo[initData.newitems] = count.items;
					} else {
						storedInfo[initData.items] = count.items;
						storedInfo[initData.newitems] = 0;
					}
					updateInfo(storedInfo);
					GM_storeObject('selected' + initData.gm_keyPostfix + '.' + id, storedInfo);
					resetPostsIcon(initData.gm_keyPostfix, id);
					if (count.pending) {
						notifyNewPosts(initData.gm_keyPostfix, id, count.items);
					}
					updateIcon(storedInfo);
				});
			} else {
				if (storedInfo[initData.newitems] > 0) {
					notifyNewPosts(initData.gm_keyPostfix, id, storedInfo[initData.newItems]);
				} else {
					resetPostsIcon(initData.gm_keyPostfix, id);
				}
			}
		} else {
			notifyError(initData.gm_keyPostfix, id, "key present, but no data");
		}
		addMinusIcon(watchType);
	} else {
		addPlusIcon(watchType);
	}
}

function processPhotoPage() {
	var rePhotoMatch = /.*flickr.com\/photos\/([^\/]*)\/(\d+)/;
	var username = rePhotoMatch.exec(document.location.href)[1];
	var photoId = rePhotoMatch.exec(document.location.href)[2];

	watchTypes['photo'].id = photoId;
	watchTypes['photo'].username = username;

	processSomethingPage({
		type: 'photo',
		id: photoId,
		colorizeFunction: function(newComments) {
			colorizeNewPostsInPhotoPage(newComments);
		},
		countItemsFunction: countCommentsInPhotoPage,
		lastPageFunction: function() {
			return lastPageInPhotoPage();
		},
		updateInfo: function(storedInfo) {
			var main = getMain();
			if ($chk(main)) {
				var title = main.getElement('h1').get('html');
			} else { // new photo page
				title = $('content').getElement('span.photo-title').get('html');
			}
			storedInfo.title = title;
		}
	});
}

function countPhotosInPhotostream(callback) {
	var photos = 0;
	var paginator = $$('div.Paginator');
	if (!$chk(paginator) || paginator.length === 0) {
		// count the replies
		photos = $$('div.photo-display-item').length;
	} else {
		var results = $$('div.Results')[0];
		photos = results.get('html').match(/\(([\d\.,]+) [^\d]+\)/)[1];
		photos = parseInt(photos.replace(/(\.|,)/g, ''), 10);
	}
	callback({ success: true, items: photos });
}

function processPhotostreamPage() {
	try {
		var nsid = GM_getPhotostreamOwnerNsid();
		if (!$chk(nsid)) { // no photos yet
			var photostreamUrl = $('SubNav').getElement('td.Buddy a').get('href');
			var reNsidMatch = /.*\/(\d+@\w\d{2})/;
			if (photostreamUrl.match(reNsidMatch)) {
				nsid = reNsidMatch.exec(photostreamUrl)[1];
			} else {
				var buddyIcon = $('SubNav').getElement('td.Buddy a img');
				if ($chk(buddyIcon)) {
					var buddyIconUrl = buddyIcon.get('src');
					nsid = reNsidMatch.exec(buddyIconUrl)[1];
				}
			}
		}
		var rePhotostreamMatch = /.*flickr.com\/photos\/([^\/]*)\//;
		var username = rePhotostreamMatch.exec(document.location.href)[1];

		watchTypes.photostream.id = nsid;
		watchTypes.photostream.username = username;

		processSomethingPage({
			type: 'photostream',
			id: nsid,
			countItemsFunction: countPhotosInPhotostream,
			updateInfo: function(storedInfo) {
				storedInfo.username = username;
			}
		});

	} catch(e) {
		GM_log("error processing photostream page: " + e);
	}
}

function countMembersInGroup(callback) {
	var members = 0;
	$$('h2').get('html').each(function(count) { // 'count' contains 'admins(n)'
		try {
			var num = /\w+\s*\(\s*([\d\.,]+)\s*\)/.exec(count)[1];
			members += parseInt(num.replace(/(\.|,)/, ''), 10);
		} catch(e) {
			callback({ success: false, message: "error parsing count on members page: " + e });
		}
	});
	callback({ success: true, items: members });
}

function processGroupMembersPage() {
	var reGroupmembersMatch = /.*flickr.com\/\/?groups_members.gne.*id=(\d+@\w\d{2})/;
	if (reGroupmembersMatch.test(document.location.href)) {
		var groupId = reGroupmembersMatch.exec(document.location.href)[1];
	} else {
		var href = getMain().getElement('a[href*=groups_members_detail.gne]').href;
		reGroupmembersMatch = /groups_members_detail.gne.*id=(\d+\@\w\d{2})/;
		groupId = reGroupmembersMatch.exec(href)[1];
	}
	var groupname = readGroupTitleFromPage();

	watchTypes.groupmembers.id = groupId;
	watchTypes.groupmembers.title = groupname;

	processSomethingPage({
		type: 'groupmembers',
		id: groupId,
		countItemsFunction: countMembersInGroup,
		updateInfo: function(storedInfo) {
			storedInfo.title = groupname;
		}
	});
}

function countReversedContacts(element, callback) {
	var members = 0;
	var paginator = element.getElement('div.Paginator');
	if (!$chk(paginator) || paginator.length === 0) {
		// count the members
		members = element.getElements('td.contact-list-name').length;
	} else {
		var results = element.getElement('div.Results');
		members = results.get('html').match(/\(([\d\.,]+) [^\d]+\)/)[1];
		members = parseInt(members.replace(/(\.|,)/g, ''), 10);
	}
	callback({ success: true, items: members });
}

function processReversedContactsPage() {
	var userNsid = GM_getUserNsid();
	var title = 'Your reverse contacts';

	watchTypes.reversedcontacts.id = userNsid;
	watchTypes.reversedcontacts.title = title;

	processSomethingPage({
		type: 'reversedcontacts',
		id: userNsid,
		countItemsFunction: function(callback) {
			countReversedContacts($(document), callback);
		},
		updateInfo: function(storedInfo) {
			storedInfo.title = title;
		}
	});
}

function countPendingItems(element, callback) {
	var items = 0;
	var paginator = element.getElement('div.Paginator');
	if ($chk(paginator) && paginator.length > 0) {
		var results = element.getElement('div.Results');
		items = results.get('html').match(/\(([\d\.,]+) [^\d]+\)/)[1];
		items = parseInt(items.replace(/(\.|,)/g, ''), 10);
	} else {
		// count the items
		items = element.getElements('td.gPendPic').length;
	}
	callback({ success: true, items: items, pending: true });
}

function countPendingTestimonials(element, callback) {
	var items = 0;
	callback({ success: true, items: element.getElements('input[name*=approve]').length, pending: true });
}

function countPendingMembers(element, callback) {
	try {
		var items = element.getElement('td[id=GoodStuff]').getElement('h3').get('text'); //getElements('form').length;
	} catch(e) {
		items = 0;
	}
	callback({ success: true, items: parseInt(items, 10), pending: true });
}

function readGroupTitleFromPage(element) {
	
	var title = "Group title not found";
	if (!$chk(element)) {
		element = document;
	}
	if ($chk(element.getElement('*[id=cattington_outer]'))) {
		try {
			return element.getElement('*[id=cattington_outer]').getElement('h1 a').get('html');
		} catch (e) {
			return element.getElement('*[id=cattington_outer]').getElement('h1').get('html').trim();
		}
	}
	if ($chk(element.getElement('*[id=sg_group_title_bg]'))) {
		return  element.getElement('*[id=sg_group_title_bg]').get('html');
	}
	if (element.getElements('h1.group-title span').length > 0) {
		var titleSpan = element.getElement('h1.group-title span').clone();
		titleSpan.removeChild(titleSpan.getElement('a'));
		return titleSpan.get('text');
	}
	
	return element.getElement('div[id=Main]').getElement('h1').getElements('a')[1].get('html');
}

function processPendingItemsPage() {
	var reGroupnameMatch = /.*flickr.com\/groups\/([^\/]*)\//;
	var groupname = reGroupnameMatch.exec(document.location.href)[1];
	var title = readGroupTitleFromPage();

	watchTypes.pendingitems.id = groupname;
	watchTypes.pendingitems.title = title;

	processSomethingPage({
		type: 'pendingitems',
		id: groupname,
		countItemsFunction: function(callback) {
			countPendingItems($(document), callback);
		},
		updateInfo: function(storedInfo) {
			storedInfo.title = title;
		}
	});
}
function processPendingTestimonialsPage() {
	var userNsid = GM_getUserNsid();
	var title = 'Your testimonals';

	watchTypes.pendingtestimonials.id = userNsid;
	watchTypes.pendingtestimonials.title = title;

	processSomethingPage({
		type: 'pendingtestimonials',
		id: userNsid,
		countItemsFunction: function(callback) {
			countPendingTestimonials($(document), callback);
		},
		updateInfo: function(storedInfo) {
			storedInfo.title = title;
		}
	});
}
function processPendingMembersPage() {
	// TODO: check for p.Problem in processXXXPage functions!
	var reGroupIdMatch = /.*flickr.com\/groups_pending\.gne\?id=(\d+\@\w\d{2})/;
	var groupId = reGroupIdMatch.exec(document.location.href)[1];
	
	try {
		var title = $('Tertiary').getElements('a')[1].get('html');
	} catch (e) {
		if ($$('p.Problem').length > 0) {
			title = $$('p.Problem')[0].get('text');
		} else {
			title = e;
		}
	}

	watchTypes.pendingmembers.id = groupId;
	watchTypes.pendingmembers.title = title;

	processSomethingPage({
		type: 'pendingmembers',
		id: groupId,
		countItemsFunction: function(callback) {
			countPendingMembers($(document), callback);
		},
		updateInfo: function(storedInfo) {
			storedInfo.title = title;
		}
	});
}

function lastPageInAppPage() {
	var lastpage = false;
	// if there is only 1 page, it's the last :-)
	var paginator = $$('div.Paginator');
	if (paginator.length === 0) {
		lastpage = true;
	} else {
		if ($chk(paginator.getElement('span.AtEnd'))) {
			lastpage = true;
		}
	}
	return lastpage;
}

function colorizeNewPostsInAppPage(newPosts) {
	if (newPosts && newPosts > 0) {
		// colorize the new posts (if on the last page)
		if (preferences.colorizeNewPosts() && lastPageInAppPage()) {
			var replyElements = $('DiscussPhoto').getElements('div.comment-owner');
			// colorize the unread ones
			replyElements.each(function(replyElement, idx) {
				if (idx < replyElements.length - newPosts) {
					return;
				}
				replyElement.addClass('NFPN_hint');
			});
			if (replyElements.length < newPosts && $$('div.Paginator').length > 0) { // also color the 'prev' button
				$$('a.Prev').addClass('NFPN_hint');
			}
		}
	}
}

function countCommentsInAppPage(element, callback) {
	var comments = 0;
	var paginator = element.getElements('div.Paginator');
	if ($chk(paginator) && paginator.length > 0) {
		var results = element.getElement('div.Results');
		comments = results.get('html').match(/\(([\d\.,]+) [^\d]+\)/)[1];
		comments = parseInt(comments.replace(/(\.|,)/g, ''), 10);
	} else {
		// count the replies
		comments = element.getElements('.comment-block').length;
	}
	callback({ success: true, items: comments });
}

function processAppPage() {
	// TODO: remove trailing \/
	// some folks provide links within Flickr without the trailing /
	var reAppidMatch = /.*flickr.com\/services\/apps\/(\d+)\//;
	var appId = reAppidMatch.exec(document.location.href)[1];
	var title = getMain().getElement('h1').get('html');
	var username = $('ag-owner-attr').getElement('b').get('html');

	watchTypes.app.id = appId;
	watchTypes.app.username = username;
	watchTypes.app.title = title;

	processSomethingPage({
		type: 'app',
		id: appId,
		colorizeFunction: function(newComments) {
			colorizeNewPostsInAppPage(newComments);
		},
		countItemsFunction: function(callback) {
			countCommentsInAppPage($(document),callback);
		},
		lastPageFunction: function() {
			return lastPageInAppPage();
		},
		updateInfo: function(storedInfo) {
			storedInfo.username = username;
			storedInfo.title = title;
		}
	});
}

function lastPageInSetPage() {
	// TODO: check for a set with multiple page comments
	return lastPageInAppPage();
}

function constructSetUrl(setId, callback) {
	new Request({
		url: 'http://www.flickr.com',
		onSuccess: function(responseText, responseXML) {
			var result = JSON.parse(responseText);
			if (result.stat === 'fail') {
				callback({ success: false, message: "error reading set information: " + result.message });
				return;
			}
			var ownerid = result.photoset.owner;
			resolveUsername(ownerid, function (user) {
				if (!user.success) {
					callback(user);
				} else {
					var username = user.displaypath;
					callback({ success: true, url: 'http://www.flickr.com/photos/' + ($chk(username) ? username : ownerid) + '/sets/' + setId + '/comments/' });
				}
			});
		},
		onFailure: function(response) {
			callback({ success: false, message: "error constructing set url " + response.statusText});
		}
	}).get('/services/rest', {
		api_key: GM_getMagisterLudi(),
		auth_hash: GM_getAuthHash(),
			csrf: GM_getCsrf(),
		auth_token: GM_getAuthToken(),
		format: 'json',
		method: 'flickr.photosets.getInfo',
		nojsoncallback: 1,
		photoset_id: setId
	});
}

function colorizeNewPostsInSetPage(newPosts) {
	if (newPosts && newPosts > 0) {
		// colorize the new posts (if on the last page)
		if (preferences.colorizeNewPosts() && lastPageInSetPage()) {
			var replyElements = $('ViewSet').getElement('table.SetComments').getElements('td.Comment');
			// colorize the unread ones
			replyElements.each(function(replyElement, idx) {
				if (idx < replyElements.length - newPosts) {
					return;
				}
				replyElement.getParent('tr').getElement('td').addClass('NFPN_hint');
			});
			if (replyElements.length < newPosts && $$('div.Paginator').length > 0) { // also color the 'prev' button
				$$('a.Prev').addClass('NFPN_hint');
			}
		}
	}
}

function countCommentsInSetPage(element, callback) { // TODO: check for multipage comments
	var comments = 0;
	var paginator = element.getElements('div.Paginator');
	if ($chk(paginator) && paginator.length > 0) {
		var results = element.getElement('div.Results');
		comments = results.get('html').match(/\(([\d\.,]+) [^\d]+\)/)[1];
		comments = parseInt(comments.replace(/(\.|,)/g, ''), 10);
	} else {
		// count the replies
		comments = element.getElements('td.Comment').length;
	}
	callback({ success: true, items: comments });
}

function processSetPage() {
	var reSetidMatch = /.*flickr.com\/photos\/([^\/]+)\/sets\/(\d+)\/comments/;
	var matches = reSetidMatch.exec(document.location.href);
	var username = matches[1];
	var setId = matches[2];
	var title = getMain().getElement('h1').get('html').replace(/^\s*(.*)\s*$/, "$1"); // remove leading and trailing white-space
	var realname = $('setCrumbs').getElement('td.scTopCrumbShareBreadcrumbs').getElement('a').get('text');

	watchTypes.set.id = setId;
	watchTypes.set.username = username;
	watchTypes.set.title = title;

	processSomethingPage({
		type: 'set',
		id: setId,
		colorizeFunction: function(newComments) {
			colorizeNewPostsInSetPage(newComments);
		},
		countItemsFunction: function(callback) {
			countCommentsInSetPage($(document), callback);
		},
		lastPageFunction: function() {
			return lastPageInSetPage();
		},
		updateInfo: function(storedInfo) {
			storedInfo.username = username;
			storedInfo.realname = realname;
			storedInfo.title = title;
		}
	});
}

function countPhotosInGroupPage(callback) {
	var photos = 0;
	var paginator = $$('div.Paginator');
	if (!$chk(paginator) || paginator.length === 0) {
		// count the photos
		try {
			photos = $$('.pool-photo').length;
		} catch(e) {
			callback({ success: false, message: "no photos found" });
		}
	} else {
		var results = $$('div.Results')[0];
		photos = results.get('html').match(/\(([\d\.,]+) [^\d]+\)/)[1];
		photos = parseInt(photos.replace(/(\.|,)/g, ''), 10);
	}
	callback({ success: true, items: photos });
}

function processGroupPoolPage() {
	var title = readGroupTitleFromPage();
	var groupId = GM_getGroupId();

	watchTypes.grouppool.id = groupId;
	watchTypes.grouppool.title = title;

	processSomethingPage({
		type: 'grouppool',
		id: groupId,
		countItemsFunction: countPhotosInGroupPage,
		updateInfo: function(storedInfo) {
			storedInfo.title = title;
		}
	});
}

function countFavoritesForUser(callback) {
	var userNsid = GM_getUserNsid();
	var favorites = 0;
	var apiData = {
		api_key: GM_getMagisterLudi(),
		auth_hash: GM_getAuthHash(),
			csrf: GM_getCsrf(),
		auth_token: GM_getAuthToken(),
		format: 'json',
		method: 'flickr.favorites.getList',
		per_page: 1,
		nojsoncallback: 1,
		user_id: userNsid
	}
	new Request({
		url: 'http://www.flickr.com',
		onSuccess: function(responseText, responseXML) {
			try {
				var result;
				try {
					result = JSON.parse(responseText);
				} catch(e) {
					try {
						result = eval('(' + responseText + ')');
					} catch (f) {
						GM_log("error parsing favorites for user: " + e);
						callback({ success: false, message: "error parsing favorites for user: " + e });
						return;
					}
				}
				if (result.stat === 'fail') {
					GM_log("error fetching user favorites: " + result.message);
					callback({ success: false, message: "error fetching user favorites: " + result.message });
					return;
				}
				favorites = result.photos.total;
				callback({ success: true, items: favorites });

			} catch(e) {
				GM_log("exception in countFavoritesForUser: " + e);
				callback({ success: false, message: "exception in countFavoritesForUser: " + e });
			}
		},
		onFailure: function(response) {
			try {
				callback({ success: false, message: "failure in countFavoritesForUser: " + response.statusText });
				GM_log("reading Favorites." + userNsid + " failed: " + response.statusText);
			} catch(e) {
				callback({ success: false, message: "exception in failure in countFavoritesForUser: " + e });
				GM_log("error reading Favorites : " + e);
			}
		}
	}).get('/services/rest', apiData);
}

function processFavoritesPage() {
	var userNsid = getMain().getElement('div.photo-display-item').get('data-fave-owner');

	var prefix = 'selected' + preferencesInitData.favorites.gm_keyPostfix;
	var favoritesInfo = GM_getObject(prefix + '.' + userNsid);
	if (favoritesInfo.key_present) {
		spinNewPostsIcon(prefix, userNsid);
		
		resolveUsername(userNsid, function(resolvedUsername) {
			var path = resolvedUsername.userpath;
			var userpath = $chk(path) ? path : userNsid;
			var displayname = resolvedUsername.displayname;

			processSomethingPage({
				type: 'favorites',
				id: userNsid,
				countItemsFunction: countFavoritesForUser,
				updateInfo: function(storedInfo) {
					storedInfo.userpath = userpath;
					storedInfo.title = displayname;
					GM_storeObject(prefix + '.' + userNsid);
					resetPostsIcon(prefix, userNsid);
				}
			});
		});
	}
}

function isGroupAdministratorOrModerator(userNsid, groupId, callback) {
	new Request({
		url: "http://www.flickr.com/",
		onFailure: function(response) {
			GM_log("failure reading members from group: " + response.statusText);
			return {
				administrator: false,
				moderator: false
			};
		},
		onSuccess: function(responseText, responseXML) {
			var result;
			try {
				result = JSON.parse(responseText);
			} catch(e) {
				result = eval('(' + responseText + ')');
			}
			if (result.stat === 'fail') {
				GM_log("failed reading members from group: " + result.code + " - " + result.message);
				return;
			}
			var retval = {
				administrator: false,
				moderator: false
			};
			var members = result.members;
			$each(members.member, function(member) {
				if (member.nsid === userNsid) {
					if (member.membertype == 3) { // moderator
						retval.moderator = true;
					} else if (member.membertype == 4) { // administrator
						retval.administrator = true;
					}
				}
			});
			callback(retval);
		}
	}).get("/services/rest", {
		api_key: GM_getMagisterLudi(),
		auth_hash: GM_getAuthHash(),
			csrf: GM_getCsrf(),
		auth_token: GM_getAuthToken(),
		format: 'json',
		nojsoncallback: 1,
		method: 'flickr.groups.members.getList',
		group_id: groupId,
		membertypes: '3,4',
		per_page: 500 // one page should be sufficient?
	});
}

function processGroupPage() { // a very special case
	var title = readGroupTitleFromPage();
	var groupId = GM_getGroupId();

	var reGroupnameMatch = /.*flickr.com\/groups\/([^\/]+)/;
	var groupname = reGroupnameMatch.exec(document.location.href)[1];

	watchTypes.group.id = groupname;

	watchTypes.groupmembers.id = groupId;
	
	watchTypes.grouppool.id = groupId;

	watchTypes.pendingitems.id = groupname;

	watchTypes.pendingmembers.id = groupId;

	var userNsid = GM_getUserNsid();
	
	isGroupAdministratorOrModerator(userNsid, groupId, function(admod) {
		[ { type: 'group', admod: false },
		  { type: 'pendingitems', admod: true },
		  { type: 'pendingmembers', admod: true },
		  { type: 'grouppool', admod: false },
		  { type: 'groupmembers', admod: false } ].forEach( function (wt) {
		  	var watchType = watchTypes[wt.type];
			watchType.title = watchType.gm_keyPostfix + ': ' + title;
			watchType.fromGroupPage = true;

			// add +/- icons for different group pages
			if (wt.admod) {
				if (!admod.administrator && !admod.moderator) {
					return;
				}
			}
			var gm_keyPostfix = watchType.gm_keyPostfix;
			var id = watchType.id;

			metaInfo = GM_getObject('selected' + gm_keyPostfix + '.' + id);

			if (metaInfo.key_present) {
				addMinusIcon(watchType);
			} else {
				addPlusIcon(watchType);
			}
		});
	});
	if ($chk($('global-nav'))) { // new navigation bar
		processGroupPoolPage();
	}
}

if (window.name === 'Log page') {
	throw new Error("not processing log page");
}

// version update info
var backgroundPort = chrome.extension.connect({ name: "flickrnotifications" });
backgroundPort.onMessage.addListener(function(msg) {
	if (msg.stat == 'error') {
		GM_log("ERROR: " + msg.statusText);
	}
	$(msg.callbackId).fireEvent(msg.callbackEvent, msg);
});

function getVersion(callbackId, callbackEvent) {
	try {
		backgroundPort.postMessage({ name: 'getVersion', callbackEvent: callbackEvent, callbackId: callbackId });
	} catch(e) {
		GM_log("ERROR connecting to the background page: " + e);
	}
}

	new Element('span', {
		id: 'NFPN-updatenotifier',
		styles: {
			display: 'none',
			visibility: 'hidden'
		},
		events: {
			'checkversion': function (evt) {

				var lastVersion = GM_getValue("version");
				if ($chk(lastVersion) && lastVersion != evt.version) {
					// cleanup
					GM_deleteValue("version");
					GM_deleteValue("lastVersionCheckTime");
					GM_deleteValue("onlineVersion");
				}
				GM_setValue("version", evt.version);
				checkVersion(evt.version);
			}
		}
	}).inject($$('body')[0], 'bottom');
	getVersion('NFPN-updatenotifier', 'checkversion');
// end version update info

preferences = new NFPNPreferences();

// first call checkForNewPosts; it triggers the creation of the envelopes
if (document.location.href.test('http://www.flickr.com') || document.location.href.test('https://www.flickr.com') || document.location.href.test('https://secure.flickr.com')) {
	checkForNewPosts();
}

if (document.location.href.test(/www.flickr.com\/groups\/[^\/]+\/discuss\/\d+/)) {
	if (!document.location.href.test(/www.flickr.com\/groups\/[^\/]+\/discuss\/\d+\/\d+\//) &&
	    !document.location.href.test(/www.flickr.com\/groups\/[^\/]+\/discuss\/\d+\/#reply/)) {
		processThreadPage();
	}

} else if (document.location.href.test(/www.flickr.com\/help\/forum\/[^\/]+\/\d+/)) {
	processForumItemPage();

} else if (document.location.href.test(/www.flickr.com\/groups\/[^\/]+\/discuss/)) {
	if (!document.location.href.test(/www.flickr.com\/groups\/[^\/]+\/discuss\/\d+/) &&
	    !document.location.href.test(/www.flickr.com\/groups\/[^\/]+\/discuss\/page\d+/)) {
		processDiscussPage();
	}

} else if (document.location.href.test(/www.flickr.com\/help\/forum\/[^\/]+/)) {
	if (!document.location.href.test(/www.flickr.com\/help\/forum\/[^\/]+\/\?page=\d+/)) {
		processHelpForumPage();
	}

} else if (document.location.href.test(/www.flickr.com\/photos\/[^\/]+\/\d+/)) {
	if (!document.location.href.test(/www.flickr.com\/photos\/[^\/]+\/\d+\/stats/) &&
	    !document.location.href.test(/www.flickr.com\/photos\/[^\/]+\/\d+\/groups/) &&
	    !document.location.href.test(/www.flickr.com\/photos\/[^\/]+\/\d+\/galleries/) &&
	    !document.location.href.test(/www.flickr.com\/photos\/[^\/]+\/\d+\/favorites/)) {
		processPhotoPage();
	}

	// skip ../photos/friends
} else if (document.location.href.test(/www.flickr.com\/photos\/(friends|upload)/)) {
	// just skip
	// skip ../photos/username/stats
} else if (document.location.href.test(/www.flickr.com\/photos\/[^\/]+\/(?:page\d+)?$/)) {
	if (!document.location.href.test(/www.flickr.com\/photos\/[^\/]+\/\d+/)) {
		processPhotostreamPage();
	}

} else if (document.location.href.test(/www.flickr.com\/photos\/[^\/]+\/favorites/)) {
	processFavoritesPage();

} else if (document.location.href.test(/www.flickr.com\/groups_members.gne.*(?!tab=)/) ||
	   document.location.href.test(/www.flickr.com\/groups\/[^\/]+\/members\/$/)) {
	processGroupMembersPage();

} else if (document.location.href.test(/www.flickr.com\/groups\/[^\/]+\/pool\/(?!page)/)) {
	if (document.location.href.test(/www.flickr.com\/groups\/[^\/]+\/pool\/\?donepending=1/)) {
		processPendingItemsPage();
	} else {
		if (!document.location.href.test(/\/pool\/tags\//) &&
		    !document.location.href.test(/donepending=1/)) {
			processGroupPoolPage();
		}
	}

} else if (document.location.href.test(/www.flickr.com\/people\/[^\/]+\/contacts\/rev/)) {
	processReversedContactsPage();

} else if (document.location.href.test(/www.flickr.com\/groups\/[^\/]+\/admin\/pending\/?(?!page)/)) {
	processPendingItemsPage();

} else if (document.location.href.test(/www.flickr.com\/services\/apps\/\d+/)) {
	if (!document.location.href.test(/www.flickr.com\/services\/apps\/\d+\/key/)) {
		processAppPage();
	}

} else if (document.location.href.test(/www.flickr.com\/photos\/[^\/]+\/sets\/\d+\/comments/)) {
	processSetPage();

} else if (document.location.href.test(/www.flickr.com\/testimonials_manage.gne/)) {
	processPendingTestimonialsPage();

} else if (document.location.href.test(/www.flickr.com\/groups_pending.gne\?id=\d+\@\w\d{2}/)) {
	processPendingMembersPage();

} else if (document.location.href.test(/www.flickr.com\/groups\/[^\/]+\/?$/)) {
	processGroupPage();

}

